import os import subprocess import time import threading import requests import random import datetime import string import json import sys import msvcrt import tkinter as tk from tkinter import scrolledtext, ttk, messagebox import uiautomator2 as u2 from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry # Импорт из poisk.py from poisk import find_and_click_last_element semaphore = threading.Semaphore(20) ADB_PATH = r"C:\Users\1\Desktop\VS 2022\Эмулятор телефона\adb" SCRCPY_PATH = r"C:\Users\1\Desktop\VS 2022\Эмулятор телефона\scrcpy" os.environ['PATH'] += os.pathsep + ADB_PATH + os.pathsep + SCRCPY_PATH def load_last_provider(): try: with open("last_provider.txt", "r", encoding="utf-8") as f: provider = f.read().strip() if provider in ["9GrizzlySMS", "SMSBower", "SMSMan", "AliSMS"]: provider_var.set(provider) else: log_message(f"Неверный провайдер: {provider}. Установлен: 9GrizzlySMS") provider_var.set("9GrizzlySMS") except: provider_var.set("9GrizzlySMS") def save_last_provider(): try: with open("last_provider.txt", "w", encoding="utf-8") as f: f.write(provider_var.get()) except Exception as e: log_message(f"Ошибка сохранения провайдера: {e}") DELAY_AFTER_SCRCPY = 1 DELAY_AFTER_LAUNCH = 1 DELAY_AFTER_CLICK = 1 DELAY_AFTER_SCROLL = 1 DELAY_BEFORE_LAST_ELEMENT = 1 DELAY_BETWEEN_DIGITS = 0.05 DELAY_AFTER_LOGIN_BUTTON = 1 DELAY_AFTER_NEGATIVE_BUTTON = 1 DELAY_AFTER_CONTINUE_BUTTON = 1 DELAY_AFTER_NAME_INPUT = 1 DELAY_AFTER_BIRTHDAY_INPUT = 1 API_KEY_FILES = { "9GrizzlySMS": "grizzly_api_key.txt", "SMSBower": "smsbower_api_key.txt", "SMSMan": "smsman_api_key.txt", "AliSMS": "alisms_api_key.txt" } API_KEYS = {} for provider, file in API_KEY_FILES.items(): try: with open(file, "r", encoding="utf-8") as f: key = f.read().strip() API_KEYS[provider] = key except: API_KEYS[provider] = "" with open(file, "w", encoding="utf-8") as f: pass SERVICE = "vk" COUNTRY_DEFAULT_PRIMARY = "Таиланд" COUNTRY_DEFAULT_SECONDARY = "Австралия" def load_countries(file_path="COUNTRIES.txt"): countries = {} try: with open(file_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if line: try: country, code = line.split(":", 1) countries[country.strip()] = code.strip() except: pass except: pass return countries PROXY_FILE = "proxy.txt" try: with open(PROXY_FILE, "r", encoding="utf-8") as pf: PROXY = pf.read().strip() if not PROXY: raise ValueError("Файл proxy.txt пуст.") except Exception as e: messagebox.showerror("Ошибка прокси", f"Не удалось прочитать прокси: {e}") sys.exit() def parse_proxy(proxy_str): parts = proxy_str.split(":") if len(parts) == 4: host, port, username, password = parts return f"http://{username}:{password}@{host}:{port}", host raise ValueError("Неверный формат прокси.") try: PROXY_URL, PROXY_HOST = parse_proxy(PROXY) except Exception as e: messagebox.showerror("Ошибка прокси", f"Не удалось разобрать прокси: {e}") sys.exit() LOCK_FILE = "app.lock" lock_file_handle = None def singleton(): global lock_file_handle try: lock_file_handle = open(LOCK_FILE, "w") msvcrt.locking(lock_file_handle.fileno(), msvcrt.LK_NBLCK, 1) except: messagebox.showerror("Ошибка", "Программа уже запущена.") sys.exit() def release_singleton(): global lock_file_handle if lock_file_handle: try: msvcrt.locking(lock_file_handle.fileno(), msvcrt.LK_UNLCK, 1) lock_file_handle.close() os.remove(LOCK_FILE) except: pass singleton() LAST_LIMIT_FILE = "last_limit.txt" LAST_LIMIT_IP_FILE = "last_limit_ip.txt" LAST_PRIMARY_PROVIDER_FILE = "last_primary_provider.txt" LAST_SECONDARY_PROVIDER_FILE = "last_secondary_provider.txt" LAST_BALANCE_THRESHOLD_FILE = "last_balance_threshold.txt" # <--- ИЗМЕНЕНИЕ primary_provider_data = {} secondary_provider_data = {} connected_devices = [] current_device_index = 0 device_change_count = 0 splash_window = None progressbar = None separate_log_window = None separate_log_text = None separate_search_matches = [] separate_search_current_index = 0 active_device_screen_enabled = False COUNTRIES = load_countries() error_count = 0 search_matches = [] search_current_index = 0 scrcpy_processes = {} current_scrcpy_process = None current_device = None fetched_phone_number = fetched_activation_id = fetched_sms_code = fetched_password = None stop_requested = False stop_after_scenario = False worker_thread = None balance_logged = False scenario_run_count = 0 total_scenarios_count = 0 total_time_elapsed = 0 running = False low_balance_flag = False active_threads = 0 active_threads_lock = threading.Lock() numbers_received = numbers_canceled = sms_codes_received = scenario_restarts = 0 sms_fail_count = 0 accounts_since_last_ip = 0 current_first_name = None LAST_SELECTED_COUNTRIES_FILE = "last_selected_countries.txt" DEFAULT_SCENARIO_LIMIT = 5 DEFAULT_MAX_INDEX = 7 DEFAULT_SMS_WAIT_TIME = 35 DEFAULT_SMS_CANCEL_ATTEMPTS = 2 DEFAULT_SMS_CANCEL_DELAY = 5 DEFAULT_IP_UPDATE_TIME = 60 DEFAULT_DELAY_AFTER_ACCOUNT_FROM = 90 DEFAULT_DELAY_AFTER_ACCOUNT_TO = 120 DEFAULT_LOW_BALANCE_THRESHOLD = 15.0 # <--- ИЗМЕНЕНИЕ def save_primary_provider(): try: with open(LAST_PRIMARY_PROVIDER_FILE, "w", encoding="utf-8") as f: f.write(primary_selected_provider.get()) except Exception as e: log_message(f"Ошибка сохранения провайдера: {e}") def save_secondary_provider(): try: with open(LAST_SECONDARY_PROVIDER_FILE, "w", encoding="utf-8") as f: f.write(secondary_selected_provider.get()) except Exception as e: log_message(f"Ошибка сохранения провайдера: {e}") def load_primary_provider(): try: with open(LAST_PRIMARY_PROVIDER_FILE, "r", encoding="utf-8") as f: provider_id = f.read().strip() primary_selected_provider.set(provider_id) set_primary_provider(provider_id) except: primary_selected_provider.set("") def load_secondary_provider(): try: with open(LAST_SECONDARY_PROVIDER_FILE, "r", encoding="utf-8") as f: provider_id = f.read().strip() secondary_selected_provider.set(provider_id) set_secondary_provider(provider_id) except: secondary_selected_provider.set("") # <--- ИЗМЕНЕНИЕ: Функции для сохранения и загрузки порога баланса def save_balance_threshold(): try: with open(LAST_BALANCE_THRESHOLD_FILE, "w", encoding="utf-8") as f: f.write(str(low_balance_threshold_var.get())) except Exception as e: log_message(f"Ошибка сохранения порога баланса: {e}") def load_balance_threshold(): try: with open(LAST_BALANCE_THRESHOLD_FILE, "r", encoding="utf-8") as f: return float(f.read().strip()) except: return DEFAULT_LOW_BALANCE_THRESHOLD # <--- КОНЕЦ ИЗМЕНЕНИЯ def get_retry_session(): session = requests.Session() retry = Retry(total=3, read=3, connect=3, backoff_factor=1, status_forcelist=[500, 502, 504], allowed_methods=["GET", "POST"]) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter) return session retry_session = get_retry_session() def log_message(message): global separate_log_text exclude_phrases = ["Ответ getPrices от SMSBower", "Распарсенные данные:", "Данные providers:"] if any(phrase in message for phrase in exclude_phrases): return timestamp = time.strftime('%Y-%m-%d %H:%M:%S') full_message = f"{timestamp} - {message}\n" log_text.config(state='normal') log_text.insert(tk.END, full_message) if auto_scroll_var.get(): log_text.see(tk.END) log_text.config(state='disabled') if separate_log_text: separate_log_text.config(state='normal') separate_log_text.insert(tk.END, full_message) separate_log_text.see(tk.END) separate_log_text.config(state='disabled') def show_splash_screen(): global splash_window, progressbar, root splash_window = tk.Toplevel() splash_window.title("Загрузка...") splash_window.geometry("400x120") splash_window.resizable(False, False) splash_window.attributes("-topmost", True) splash_window.update_idletasks() w, h = 400, 120 ws, hs = splash_window.winfo_screenwidth(), splash_window.winfo_screenheight() x = (ws // 2) - (w // 2) y = (hs // 2) - (h // 2) splash_window.geometry(f"{w}x{h}+{x}+{y}") label = tk.Label(splash_window, text="Пожалуйста, подождите...\nИдёт загрузка программы", font=("Helvetica", 12)) label.pack(pady=10) progressbar = ttk.Progressbar(splash_window, orient="horizontal", mode="indeterminate", length=300) progressbar.pack(pady=5) progressbar.start(10) threading.Thread(target=on_loading, daemon=True).start() def on_loading(): progressbar.config(mode="determinate", maximum=100, value=0) for i in range(101): time.sleep(0.01) progressbar["value"] = i splash_window.update_idletasks() close_splash_and_show_main() def close_splash_and_show_main(): global splash_window, progressbar if splash_window: splash_window.destroy() progressbar = None root.deiconify() def open_separate_log_window(): global separate_log_window, separate_log_text, separate_search_entry, separate_search_count_label, separate_search_index_label if separate_log_window and separate_log_window.winfo_exists(): separate_log_window.lift() return separate_log_window = tk.Toplevel(root) separate_log_window.title("Журнал") separate_log_window.geometry("900x600") def on_close(): global separate_log_window, separate_log_text, separate_search_matches, separate_search_current_index, separate_search_entry separate_log_window.destroy() separate_log_window = separate_log_text = separate_search_entry = separate_search_count_label = separate_search_index_label = None separate_search_matches = [] separate_search_current_index = 0 separate_log_window.protocol("WM_DELETE_WINDOW", on_close) search_frame = tk.Frame(separate_log_window, bg=FRAME_COLOR) search_frame.pack(fill=tk.X, padx=5, pady=5) tk.Label(search_frame, text="Поиск:", font=("Helvetica", 10), bg=FRAME_COLOR, fg=LABEL_COLOR).pack(side=tk.LEFT, padx=(0, 5)) separate_search_entry = tk.Entry(search_frame, font=("Helvetica", 10)) separate_search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5)) separate_search_entry.bind("", separate_search_log) tk.Button(search_frame, text="Предыдущий", font=("Helvetica", 10), command=separate_prev_match, bg=BUTTON_COLOR, fg=BUTTON_FOREGROUND).pack(side=tk.LEFT, padx=5) tk.Button(search_frame, text="Следующий", font=("Helvetica", 10), command=separate_next_match, bg=BUTTON_COLOR, fg=BUTTON_FOREGROUND).pack(side=tk.LEFT, padx=5) separate_search_index_label = tk.Label(search_frame, text="", font=("Helvetica", 10), bg=FRAME_COLOR, fg=LABEL_COLOR) separate_search_index_label.pack(side=tk.LEFT, padx=5) separate_search_count_label = tk.Label(search_frame, text="0 matches", font=("Helvetica", 10), bg=FRAME_COLOR, fg=LABEL_COLOR) separate_search_count_label.pack(side=tk.LEFT, padx=5) tk.Button(search_frame, text="Искать", font=("Helvetica", 10), command=separate_search_log, bg=BUTTON_COLOR, fg=BUTTON_FOREGROUND).pack(side=tk.LEFT, padx=(0,5)) separate_log_text = scrolledtext.ScrolledText(separate_log_window, state='disabled', wrap=tk.WORD, font=("Consolas", 10), bg=TEXT_BACKGROUND, fg=TEXT_FOREGROUND, insertbackground=LABEL_COLOR) separate_log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) main_log_content = log_text.get("1.0", tk.END) separate_log_text.config(state='normal') separate_log_text.insert(tk.END, main_log_content) separate_log_text.config(state='disabled') separate_log_text.bind("", lambda e: separate_text_context_menu(e, separate_log_text)) def separate_text_context_menu(event, text_widget): menu = tk.Menu(root, tearoff=0, bg=FRAME_COLOR, fg=LABEL_COLOR) if text_widget.tag_ranges("sel"): menu.add_command(label="Копировать", command=lambda: copy_to_clipboard(text_widget.get(tk.SEL_FIRST, tk.SEL_LAST))) menu.add_command(label="Выделить всё", command=lambda: (text_widget.tag_add(tk.SEL, "1.0", tk.END), text_widget.mark_set(tk.INSERT, "1.0"), text_widget.see(tk.INSERT))) menu.post(event.x_root, event.y_root) def check_stop(): if stop_requested: raise RuntimeError("Операция остановлена пользователем.") def safe_click(selector, desc, delay_from=0.5, delay_to=1.5): check_stop() d = get_current_device() if d and selector.exists(timeout=5): time.sleep(random.uniform(delay_from, delay_to)) selector.click() log_message(f"{desc} нажато.") else: log_message(f"{desc} не найдено.") time.sleep(random.uniform(delay_from, delay_to)) def run_adb_command(command): result = subprocess.run(command, shell=True, capture_output=True, text=True) if result.returncode != 0: log_message(f"Ошибка ADB: {command}\n{result.stderr}") return None return result.stdout.strip() def try_reconnect_device(serial, attempts=3): for i in range(attempts): log_message(f"Попытка переподключения #{i+1} к {serial}...") subprocess.run("adb kill-server", shell=True) time.sleep(1) subprocess.run("adb start-server", shell=True) time.sleep(2) result = subprocess.run("adb devices", shell=True, capture_output=True, text=True) if result.returncode == 0 and serial in result.stdout and "\tdevice" in result.stdout: log_message(f"Устройство {serial} переподключено.") return True time.sleep(1) log_message(f"Не удалось переподключить {serial}.") return False def copy_to_clipboard(text): root.clipboard_clear() root.clipboard_append(text) def change_ip(): global accounts_since_last_ip, device_change_count, fetched_activation_id if fetched_activation_id: log_message("Активный номер. Отмена активации перед сменой IP.") cancel_activation() start_wait = time.time() while time.time() - start_wait < 15: in_progress = any(task["activation_id"] == fetched_activation_id and task["status"] == "In progress" for task in cancellation_tasks.values()) if not in_progress: break log_message("Ожидание отмены...") time.sleep(1) log_message("Отмена завершена." if not in_progress else "Время ожидания истекло.") root.after(0, lambda: start_button.config(state=tk.DISABLED)) log_message("Запуск 'com.dualspacepro.multispace' перед сменой IP...") launch_phone_app() time.sleep(0.5) previous_ip = get_proxy_ip() log_message(f"Предыдущий IP: {previous_ip}") with open("смена ip.txt", "r", encoding="utf-8") as f: url = f.read().strip() if url: log_message(f"Смена IP: запрос {url}") loaded = False start_time = time.time() if show_browser_var.get(): import webbrowser webbrowser.open(url) log_message("Браузер открыт.") while not loaded and time.time() - start_time < 60: try: response = requests.get(url, timeout=5) if response.status_code == 200: loaded = True break except: pass time.sleep(0.5) if loaded: log_message("Страница загружена.") else: log_message("Не удалось загрузить, продолжаем...") time.sleep(0.5) new_ip = get_proxy_ip() log_message(f"Новый IP: {new_ip}") log_message(f"IP изменён: {previous_ip} -> {new_ip}") remaining_ip = ip_change_limit_var.get() remaining_ip_change_label.config(text=f"{remaining_ip} аккаунтов") accounts_since_last_ip = 0 device_change_count += 1 remaining = device_change_limit_var.get() - device_change_count remaining_device_change_label.config(text=str(remaining)) if len(connected_devices) > 1 and device_change_count >= device_change_limit_var.get(): device_change_count = 0 remaining_device_change_label.config(text=str(device_change_limit_var.get())) switch_to_next_device() delay_from = delay_after_ip_from_var.get() delay_to = delay_after_ip_to_var.get() random_delay = random.randint(delay_from, delay_to) log_message(f"Задержка после IP: {random_delay} сек.") time.sleep(random_delay) global sms_fail_count sms_fail_count = 0 remaining_sms_fail_label.config(text=str(sms_fail_limit_var.get())) else: log_message("Файл 'смена ip.txt' пуст.") root.after(0, lambda: start_button.config(state=tk.NORMAL)) def change_ip_manual(): original_check_stop = globals()['check_stop'] try: globals()['check_stop'] = lambda: None change_ip() if auto_launch_var.get(): log_message("Автозапуск после IP. Запуск...") launch_and_click() finally: globals()['check_stop'] = original_check_stop def switch_to_next_device(): global current_device_index, active_device_screen_enabled prev_serial = connected_devices[current_device_index]['serial'] if prev_serial in scrcpy_processes: try: scrcpy_processes[prev_serial].terminate() scrcpy_processes[prev_serial].wait(timeout=5) log_message(f"scrcpy для {prev_serial} завершён.") except Exception as e: log_message(f"Ошибка завершения scrcpy для {prev_serial}: {e}") del scrcpy_processes[prev_serial] current_device_index = (current_device_index + 1) % len(connected_devices) log_message(f"Переключились на: {connected_devices[current_device_index]['serial']}") update_devices_status_in_table() if active_device_screen_enabled: new_serial = connected_devices[current_device_index]['serial'] process = launch_device_screen(new_serial, 500, 1000) if process: scrcpy_processes[new_serial] = process active_toggle_button.config(text="Скрыть устройство") else: messagebox.showerror("Ошибка", f"Не удалось запустить для {new_serial}.") def get_current_device(): if not connected_devices: return None return connected_devices[current_device_index].get("u2_device", None) def update_difference_label(): difference = numbers_received - numbers_canceled - scenario_run_count root.after(0, lambda: difference_label.config(text=f"Не отменён: {difference}")) def update_devices_status_in_table(): for i, dev_info in enumerate(connected_devices): status = "Active" if i == current_device_index else "Idle" dev_info['status'] = status smartphones_tree.set(dev_info['id'], column="status", value=status) smartphones_tree.set(dev_info['id'], column="accounts", value=dev_info.get("accounts", 0)) smartphones_tree.set(dev_info['id'], column="numbers", value=dev_info.get("numbers", 0)) active_dev = connected_devices[current_device_index] active_smartphone_label.config(text=f"Активный: {active_dev['serial']} ({active_dev['model']})") def select_smartphone_as_first(): global current_device_index selection = smartphones_tree.selection() if not selection: messagebox.showinfo("Выбор", "Выделите строку.") return selected_id = int(selection[0]) for idx, dev_info in enumerate(connected_devices): if dev_info['id'] == selected_id: current_device_index = idx break log_message(f"Смартфон ID={selected_id} выбран первым.") update_devices_status_in_table() def update_devices_display_status(): for dev in connected_devices: item_id = str(dev['id']) serial = dev['serial'] smartphones_tree.item(item_id, tags=("shown",) if serial in scrcpy_processes else ()) def scan_devices(): global connected_devices connected_devices.clear() smartphones_tree.delete(*smartphones_tree.get_children()) result = run_adb_command("adb devices -l") if not result: log_message("Ошибка получения устройств.") return lines = result.splitlines() device_id_seq = 1 for line in lines[1:]: line = line.strip() if not line or "offline" in line or "unauthorized" in line or "unknown" in line: continue parts = line.split() if len(parts) < 2: continue serial = parts[0] model = next((p.split(":", 1)[1] for p in parts if p.startswith("model:")), "Unknown") dev_info = { "id": device_id_seq, "serial": serial, "model": model, "status": "Idle", "u2_device": None, "accounts": 0, "numbers": 0 } connected_devices.append(dev_info) smartphones_tree.insert("", "end", iid=str(device_id_seq), values=(device_id_seq, serial, model, "Idle", 0, 0)) device_id_seq += 1 if connected_devices: for i, dev_info in enumerate(connected_devices): try: d = u2.connect(dev_info['serial']) dev_info['u2_device'] = d log_message(f"Устройство {dev_info['serial']} ({dev_info['model']}) подключено.") except Exception as e: log_message(f"Не удалось подключиться к {dev_info['serial']}: {e}") current_device_index = 0 update_devices_status_in_table() else: log_message("Нет устройств.") def launch_phone_app(): check_stop() d = get_current_device() if not d: log_message("Нет смартфона.") return log_message("Запуск приложения...") package = "com.dualspacepro.multispace" cmd = f"adb -s {d.serial} shell monkey -p {package} -c android.intent.category.LAUNCHER 1" try: subprocess.run(cmd, shell=True, check=True) log_message(f"{package} запущено на {d.serial}.") time.sleep(1) except subprocess.CalledProcessError as e: error_text = e.stderr or "" if "device offline" in error_text.lower() or "device offline" in str(e).lower(): log_message(f"{d.serial} offline. Переподключение...") if not try_reconnect_device(d.serial): log_message("Не удалось, переключаемся.") switch_to_next_device() return log_message("Переподключено. Запуск снова.") try: subprocess.run(cmd, shell=True, check=True) log_message(f"{package} запущено после переподключения.") time.sleep(1) except subprocess.CalledProcessError as e2: log_message(f"Ошибка запуска после: {e2}") switch_to_next_device() else: log_message(f"Ошибка запуска {package}: {e}") switch_to_next_device() def click_first_element(): check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return time.sleep(random.uniform(0.5, 1.5)) safe_click(d(resourceId="com.dualspacepro.multispace:id/ivClone"), "Элемент ivClone") def scroll_down(): check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return log_message("Прокрутка вниз...") recycler = d(resourceId="com.dualspacepro.multispace:id/rvSystem") if recycler.exists(timeout=10): recycler.scroll.toEnd() log_message("Прокрутка выполнена.") else: log_message("RecyclerView не найден.") time.sleep(1) def click_element_with_vk_text(): check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return log_message("Поиск 'VK'...") # Изменено сообщение recycler = d(resourceId="com.dualspacepro.multispace:id/rvSystem") if not recycler.exists(timeout=10): log_message("RecyclerView не найден.") return found = False for attempt in range(3): for _ in range(10): check_stop() if d(textContains="VK").exists(timeout=1): # Изменено на textContains="VK" log_message("Элемент найден, нажимаем.") d(textContains="VK").click() # Изменено на textContains="VK" found = True break else: log_message("Не найден, прокручиваем.") recycler.scroll(steps=5) time.sleep(1) if found: break if not found: log_message("'VK' не найден.") # Изменено сообщение def click_clone_button(): check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return safe_click(d(resourceId="com.dualspacepro.multispace:id/tvClone"), "Кнопка 'Clone'") def click_last_element(): check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return find_and_click_last_element(d, max_index_var, log_message) def click_with_retries(click_callable, description, attempts=3, delay=1): for attempt in range(1, attempts + 1): try: if click_callable(): log_message(f"{description} успешно (попытка {attempt}).") return True except Exception as e: log_message(f"Ошибка {description}: {e}") log_message(f"Не удалось {description}, попытка {attempt}.") time.sleep(delay) log_message(f"{description} не удалось после {attempts} попыток.") return False def click_continue_button(): check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return False try: btn = d(resourceId="com.vkontakte.android:id/continue_btn") if btn.exists(timeout=10): time.sleep(random.uniform(3.5, 5.5)) btn.click() log_message("Кнопка 'Continue' нажата.") return True else: log_message("Кнопка 'Continue' не найдена.") return False except Exception as e: log_message(f"Ошибка 'Continue': {e}") return False def click_login_button(): check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return safe_click(d(resourceId="com.vkontakte.android:id/login_button"), "Кнопка 'Login'") def click_negative_button(): check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return password_field_id = "com.vkontakte.android:id/password_container" if d(resourceId=password_field_id).exists(timeout=random.uniform(0.2, 0.5)): log_message("Поле пароля найдено. Отмена номера и перезапуск...") if fetched_phone_number: cancel_activation() log_message(f"Номер {fetched_phone_number} на отмену.") restart_scenario() return negative_button = d(resourceId="com.vkontakte.android:id/negative_button") if negative_button.exists(timeout=random.uniform(12, 15)): negative_button.click() log_message("Кнопка 'Negative' нажата.") else: log_message("Кнопка 'Negative' не найдена. Координатный клик.") info = d.info width = info.get("displayWidth", 1080) height = info.get("displayHeight", 1920) x = int(0.391 * width) y = int(0.821 * height) d.click(x, y) log_message(f"Клик по ({x}, {y}). Ожидание 'Negative'...") if negative_button.wait(timeout=50): negative_button.click() log_message("Кнопка 'Negative' нажата после ожидания.") else: log_message("Не обнаружена. Отмена и перезапуск.") cancel_activation() restart_scenario() time.sleep(1) request_lock = threading.Lock() def set_primary_provider(provider_id): primary_selected_provider.set(provider_id) primary_provider_label.config(text=f"Провайдер: {provider_id}" if provider_id else "Провайдер: Не выбран") log_message(f"Выбран провайдер для первой: {provider_id}" if provider_id else "Не выбран для первой") country_code = COUNTRIES.get(primary_country_var.get(), "52") providers = primary_provider_data.get(country_code, []) if provider_id: for p in providers: if p['id'] == provider_id: set_price_and_count_labels(p['price'], p['count'], "primary") break else: set_price_and_count_labels("N/A", "N/A", "primary") else: fetch_country_price_and_count(country_code, "primary", provider_var.get()) save_primary_provider() def set_secondary_provider(provider_id): secondary_selected_provider.set(provider_id) secondary_provider_label.config(text=f"Провайдер: {provider_id}" if provider_id else "Провайдер: Не выбран") log_message(f"Выбран провайдер для второй: {provider_id}" if provider_id else "Не выбран для второй") country_code = COUNTRIES.get(secondary_country_var.get(), "143") providers = secondary_provider_data.get(country_code, []) if provider_id: for p in providers: if p['id'] == provider_id: set_price_and_count_labels(p['price'], p['count'], "secondary") break else: set_price_and_count_labels("N/A", "N/A", "secondary") else: fetch_country_price_and_count(country_code, "secondary", provider_var.get()) save_secondary_provider() def request_number(max_attempts=5, delay=5, country_delay=3): global fetched_phone_number, fetched_activation_id, numbers_received check_stop() with request_lock: log_message("Запрос номера...") provider = provider_var.get() country_codes = [ ("Первая страна", COUNTRIES.get(primary_country_var.get(), "52")), ("Вторая страна", COUNTRIES.get(secondary_country_var.get(), "143")) ] for idx, (country_name, code) in enumerate(country_codes): category = "primary" if idx == 0 else "secondary" log_message(f"Запрос для {country_name} (код: {code}) через {provider}") set_current_country_label(country_name) if provider == "9GrizzlySMS": url = f"https://api.9grizzlysms.com/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getNumber&service={SERVICE}&country={code}" elif provider == "SMSMan": url = f"https://api.sms-man.com/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getNumber&service={SERVICE}&country={code}" elif provider == "AliSMS": url = f"https://api.alisms.org/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getNumber&service={SERVICE}&country={code}" selected_provider = primary_selected_provider.get() if category == "primary" else secondary_selected_provider.get() if selected_provider: url += f"&operator={selected_provider}" log_message(f"Оператор: {selected_provider}") else: url = f"https://smsbower.online/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getNumber&service={SERVICE}&country={code}" selected_provider = primary_selected_provider.get() if category == "primary" else secondary_selected_provider.get() if selected_provider: url += f"&providerIds={selected_provider}" log_message(f"Провайдер: {selected_provider}") for attempt in range(1, max_attempts + 1): check_stop() log_message(f"Попытка {attempt}/{max_attempts} для {country_name}") try: response = retry_session.get(url, timeout=30) if response.status_code == 200: data = response.text.strip() if data.startswith("ACCESS_NUMBER"): parts = data.split(":") if len(parts) == 3: fetched_activation_id, fetched_phone_number = parts[1], parts[2] log_message(f"Номер: {fetched_phone_number}, id: {fetched_activation_id}") numbers_received += 1 update_numbers_received_label() paste_phone_number_in_app() current_dev = connected_devices[current_device_index] current_dev["numbers"] = current_dev.get("numbers", 0) + 1 update_devices_status_in_table() return True else: log_message(f"Неверный формат: {data}") elif "NO_NUMBERS" in data: log_message(f"Нет номеров, попытка {attempt}.") elif "BAD_KEY" in data: log_message("Ошибка: BAD_KEY.") return False else: log_message(f"Неизвестный ответ: {data}") else: log_message(f"Ошибка {response.status_code}: {response.text}") except requests.exceptions.RequestException as e: log_message(f"Попытка {attempt}: Ошибка: {e}") if attempt < max_attempts: log_message(f"Ожидание {delay} сек...") time.sleep(delay) log_message(f"Нет номеров для {country_name} после {max_attempts}.") if idx < len(country_codes) - 1: log_message(f"Ожидание {country_delay} сек перед следующей...") time.sleep(country_delay) log_message("Номера не получены. Перезапуск.") restart_scenario() return False def wait_for_error_or_password(timeout=7): d = get_current_device() error_message_id = "com.vkontakte.android:id/error_message" password_field_id = "com.vkontakte.android:id/password_container" error_found = password_found = False start_time = time.time() while time.time() - start_time < timeout: if d(resourceId=error_message_id).exists(timeout=0.5): error_found = True break if d(resourceId=password_field_id).exists(timeout=0.5): password_found = True break return error_found, password_found def paste_phone_number_in_app(): d = get_current_device() if not d: log_message("Устройство не подключено.") return target_id = "com.vkontakte.android:id/layout_email_or_phone" log_message("Вставка номера...") if d(resourceId=target_id).exists(timeout=35): d(resourceId=target_id).click() time.sleep(2) child = d(resourceId=target_id).child(className="android.widget.LinearLayout", index=1) if child.exists(timeout=2): child.click() if fetched_phone_number: log_message(f"Вставляем: {fetched_phone_number}") field = d(resourceId="com.vkontakte.android:id/et_email_or_phone") if not field.exists(timeout=2): field = d(resourceId=target_id).child(className="android.widget.EditText") if field.exists(timeout=2): field.set_text(fetched_phone_number) log_message("Номер вставлен.") time.sleep(random.uniform(2, 3)) click_login_button() log_message("Поиск ошибки/пароля 8 сек...") error_found, password_found = wait_for_error_or_password(8) if error_found: log_message("Ошибка при вставке.") if fetched_phone_number: cancel_activation() log_message(f"{fetched_phone_number} на отмену.") restart_scenario() return elif password_found: log_message("Поле пароля найдено.") else: log_message("Ничего не найдено за 8 сек.") click_negative_button() else: log_message("Поле ввода не найдено.") else: log_message("Номер не определён.") else: log_message("layout_email_or_phone не найдено.") def request_sms_code_async(max_attempts=5, delay=5): with semaphore: global fetched_sms_code, sms_codes_received if not fetched_activation_id: log_message("activation_id не установлен.") return False log_message("Запрос SMS-кода...") provider = provider_var.get() if provider == "9GrizzlySMS": url = f"https://api.9grizzlysms.com/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getStatus&id={fetched_activation_id}" elif provider == "SMSMan": url = f"https://api.sms-man.com/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getStatus&id={fetched_activation_id}" elif provider == "AliSMS": url = f"https://api.alisms.org/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getStatus&id={fetched_activation_id}" else: url = f"https://smsbower.online/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getStatus&id={fetched_activation_id}" max_wait = sms_wait_time_var.get() elapsed = 0 while elapsed < max_wait: try: response = retry_session.get(url, timeout=10) except requests.exceptions.RequestException as e: log_message(f"Ошибка SMS: {e}") time.sleep(delay) elapsed += delay continue if response.status_code == 200: data = response.text.strip() log_message(f"SMS ответ: {data}") if data.startswith("STATUS_OK"): parts = data.split(":") if len(parts) == 2: fetched_sms_code = parts[1] log_message(f"SMS-код: {fetched_sms_code}") sms_codes_received += 1 update_sms_codes_received_label() global device_change_count device_change_count = 0 remaining_device_change_label.config(text=str(device_change_limit_var.get())) input_sms_code() return True else: log_message(f"Неверный формат: {data}") elif data.startswith("STATUS_WAIT_CODE"): remaining = max_wait - elapsed log_message(f"Ожидание, осталось {remaining} сек.") elif "NO_NUMBERS" in data or "STATUS_TIMEOUT" in data or "STATUS_ERROR" in data: log_message(f"Ошибка SMS: {data}") cancel_activation() return False else: log_message(f"Неизвестный статус: {data}") else: log_message(f"Ошибка {response.status_code}: {response.text}") time.sleep(delay) elapsed += delay log_message(f"SMS не получен за {max_wait} сек.") cancel_activation() return False def input_sms_code(): d = get_current_device() if not d: log_message("Устройство не подключено.") return field_id = "com.vkontakte.android:id/code_edit_text_container" log_message("Ввод SMS-кода...") if d(resourceId=field_id).exists(timeout=10): d(resourceId=field_id).click() time.sleep(random.uniform(0.5, 1.5)) if fetched_sms_code: field = d(resourceId="com.vkontakte.android:id/et_sms_code") if not field.exists(timeout=5): field = d(resourceId=field_id).child(className="android.widget.EditText") if field.exists(timeout=5): field.set_text(fetched_sms_code) log_message(f"SMS {fetched_sms_code} введён.") else: log_message("Поле SMS не найдено.") else: log_message("SMS-код пуст.") else: log_message("code_edit_text_container не найдено.") cancellation_tasks = {} cancellation_task_counter = 1 cancellation_tree = None cancellation_details_text = None def update_cancellation_task(task_id, **kwargs): global cancellation_tasks if task_id in cancellation_tasks: task = cancellation_tasks[task_id] task.update(kwargs) if cancellation_tree: values = (task["id"], task["phone"], task["status"], task["attempts"], task["start"], task["end"]) root.after(0, lambda: cancellation_tree.item(str(task_id), values=values)) def append_cancellation_task_log(task_id, message): global cancellation_tasks if task_id in cancellation_tasks: task = cancellation_tasks[task_id] timestamp = time.strftime("%Y-%m-%d %H:%M:%S") task["log"] += f"{timestamp} - {message}\n" update_cancellation_task(task_id) def background_cancel_activation(task_id, activation_id, phone): global numbers_canceled attempts = sms_cancel_attempts_var.get() delay_val = sms_cancel_delay_var.get() success = False provider = provider_var.get() if provider == "SMSBower": status_code = 8 url = f"https://smsbower.online/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=setStatus&status={status_code}&id={activation_id}" elif provider == "SMSMan": status_code = -1 url = f"https://api.sms-man.com/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=setStatus&status={status_code}&id={activation_id}" elif provider == "AliSMS": status_code = 8 url = f"https://api.alisms.org/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=setStatus&status={status_code}&id={activation_id}" else: status_code = -1 url = f"https://api.9grizzlysms.com/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=setStatus&status={status_code}&id={activation_id}" for attempt in range(1, attempts + 1): append_cancellation_task_log(task_id, f"Попытка {attempt}/{attempts} для {activation_id}.") try: response = retry_session.get(url, timeout=10) if response.status_code == 200: data = response.text.strip() append_cancellation_task_log(task_id, f"Ответ: {data}") if data.startswith("ACCESS_CANCEL"): numbers_canceled += 1 update_numbers_canceled_label() success = True update_cancellation_task(task_id, attempts=attempt, status="Canceled", end=datetime.datetime.now().strftime("%H:%M:%S")) append_cancellation_task_log(task_id, "Отменено успешно.") break else: append_cancellation_task_log(task_id, f"Неудача {attempt}: {data}") else: append_cancellation_task_log(task_id, f"Ошибка {response.status_code}: {response.text}") except Exception as e: append_cancellation_task_log(task_id, f"Ошибка: {e}") if not success and attempt < attempts: time.sleep(delay_val) if not success: update_cancellation_task(task_id, attempts=attempts, status="Failed", end=datetime.datetime.now().strftime("%H:%M:%S")) append_cancellation_task_log(task_id, "Отмена не удалась.") def spawn_cancel_task(activation_id, phone): global cancellation_task_counter, cancellation_tasks task_id = cancellation_task_counter cancellation_task_counter += 1 task = { "id": task_id, "activation_id": activation_id, "phone": phone, "attempts": 0, "status": "In progress", "log": "", "start": datetime.datetime.now().strftime("%H:%M:%S"), "end": "" } cancellation_tasks[task_id] = task if cancellation_tree: root.after(0, lambda: cancellation_tree.insert("", "end", iid=str(task_id), values=(task_id, phone, task["status"], task["attempts"], task["start"], task["end"]))) threading.Thread(target=background_cancel_activation, args=(task_id, activation_id, phone), daemon=True).start() def cancel_activation(): global fetched_activation_id, fetched_phone_number if not fetched_activation_id: log_message("activation_id не установлен.") return local_activation_id = fetched_activation_id local_phone = fetched_phone_number fetched_activation_id = fetched_phone_number = None spawn_cancel_task(local_activation_id, local_phone) def handle_sms_verification(): if request_sms_code_async(): return True log_message("SMS не получен. Отмена.") return False def get_random_name(): try: with open("Имена и Фамилии.txt", "r", encoding="utf-8") as file: lines = [line.strip() for line in file if line.strip()] if not lines: log_message("Файл имен пуст.") return None, None name_line = random.choice(lines) parts = name_line.split() if len(parts) < 2: log_message(f"Некорректно: {name_line}") return None, None log_message(f"Имя: {parts[0]} {parts[1]}") return parts[0], parts[1] except Exception as e: log_message(f"Ошибка чтения имен: {e}") return None, None def generate_password(): length = random.randint(8, 12) pwd = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length)) log_message(f"Пароль: {pwd}") return pwd def input_first_last_name(): global current_first_name check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return name, surname = get_random_name() if not name or not surname: log_message("Имя/фамилия не получены.") return first_name_id = "com.vkontakte.android:id/layout_first_name" found = False for attempt in range(1, 11): if d(resourceId=first_name_id).exists(timeout=10): found = True break log_message(f"Попытка {attempt}/10: {first_name_id} не найдено.") time.sleep(random.uniform(1.0, 2.0)) if not found: log_message(f"{first_name_id} не найдено после 10. Перезапуск.") restart_scenario() return try: d(resourceId=first_name_id).click() time.sleep(random.uniform(1.0, 2.0)) field = d(resourceId="com.vkontakte.android:id/et_first_name") if not field.exists(timeout=5): field = d(resourceId=first_name_id).child(className="android.widget.EditText") if field.exists(timeout=5): field.set_text(name) log_message(f"Имя '{name}' введено.") current_first_name = name else: log_message("Поле имени не найдено.") except Exception as e: log_message(f"Ошибка ввода имени: {e}") restart_scenario() return last_name_id = "com.vkontakte.android:id/layout_last_name" if d(resourceId=last_name_id).exists(timeout=random.uniform(7, 10)): d(resourceId=last_name_id).click() time.sleep(random.uniform(0.5, 1.5)) field = d(resourceId="com.vkontakte.android:id/et_last_name") if not field.exists(timeout=random.uniform(1, 2)): field = d(resourceId=last_name_id).child(className="android.widget.EditText") if field.exists(timeout=random.uniform(1, 2)): field.set_text(surname) log_message(f"Фамилия '{surname}' введена.") else: log_message("Поле фамилии не найдено.") else: log_message("layout_last_name не найдено.") def get_random_birthday(): start = datetime.date(1980, 1, 1) end = datetime.date(2005, 1, 1) birthday = start + datetime.timedelta(days=random.randint(0, (end - start).days)) return birthday.strftime("%d.%m.%Y") def select_gender(first_name): d = get_current_device() if not d: log_message("Устройство не подключено (select_gender).") return vowels = "aeiouyаеёиоуыэюяAEIOUYАЕЁИОУЫЭЮЯ" female = first_name[-1] in vowels radio_id = "com.vkontakte.android:id/female_gender_radio" if female else "com.vkontakte.android:id/male_gender_radio" if d(resourceId=radio_id).wait(timeout=10): d(resourceId=radio_id).click() log_message(f"Пол: {'женский' if female else 'мужской'}.") else: log_message(f"Переключатель {radio_id} не найден.") def input_birthday(): check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return birthday = get_random_birthday() log_message(f"Дата: {birthday}") field_id = "com.vkontakte.android:id/enter_birthday_container" if d(resourceId=field_id).exists(timeout=random.uniform(7, 10)): d(resourceId=field_id).click() time.sleep(random.uniform(0.5, 1.0)) field = d(resourceId="com.vkontakte.android:id/et_birthday") if not field.exists(timeout=5): field = d(resourceId=field_id).child(className="android.widget.EditText") if field.exists(timeout=5): field.set_text(birthday) log_message("Дата введена.") else: log_message("Поле даты не найдено.") else: log_message(f"{field_id} не найдено.") time.sleep(random.uniform(1.5, 2.5)) def click_continue_button_after_birthday(): click_continue_button() time.sleep(2) def input_password(): global error_count check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return field_id = "com.vkontakte.android:id/password_smart_layout" found = False for attempt in range(1, 8): if d(resourceId=field_id).exists(timeout=2): found = True break log_message(f"Попытка {attempt}/7: {field_id} не найдено.") time.sleep(1) if not found: log_message(f"{field_id} не появилось после 7. Перезапуск.") error_count += 1 update_error_count_label() cancel_activation() restart_scenario() return pwd = generate_password() global fetched_password fetched_password = pwd for field_id, et_id in [("com.vkontakte.android:id/password_smart_layout", "com.vkontakte.android:id/et_password"), ("com.vkontakte.android:id/repeat_password_smart_layout", "com.vkontakte.android:id/et_repeat_password")]: try: d(resourceId=field_id).click() time.sleep(random.uniform(0.5, 1.5)) field = d(resourceId=et_id) if not field.exists(timeout=2): field = d(resourceId=field_id).child(className="android.widget.EditText") if field.exists(timeout=2): field.set_text(pwd) log_message("Пароль введён." if "et_password" in et_id else "Повтор введён.") else: log_message(f"Поле для {field_id} не найдено.") error_count += 1 update_error_count_label() except Exception as e: log_message(f"Ошибка ввода в {field_id}: {e}") error_count += 1 update_error_count_label() cancel_activation() restart_scenario() return if fetched_phone_number and pwd: try: with open("NewLogPass.txt", "a", encoding="utf-8") as file: file.write(f"{fetched_phone_number}:{pwd}\n") log_message(f"Сохранено: {fetched_phone_number}:{pwd}") except Exception as e: log_message(f"Ошибка сохранения: {e}") error_count += 1 update_error_count_label() btn = d(resourceId="com.vkontakte.android:id/continue_btn") if btn.wait(timeout=10): time.sleep(random.uniform(3.5, 5.5)) btn.click() log_message("Кнопка 'Continue' нажата.") else: log_message("'Continue' не найдена. Перезапуск.") error_count += 1 update_error_count_label() cancel_activation() restart_scenario() return log_message("Нажатие 'Пропустить' 4 раза...") skip_count = 0 for i in range(3): check_stop() log_message(f"Пропустить ({i+1}/3)...") success = click_with_retries(lambda: d(text="Пропустить").click_exists(timeout=1), "кнопка 'Пропустить'", attempts=4, delay=random.uniform(2.5, 5.5)) if success: skip_count += 1 time.sleep(random.uniform(0.5, 1)) if skip_count == 4: log_message("'Пропустить' нажата 4 раза.") else: log_message(f"Удалось {skip_count} раз. Перезапуск.") error_count += 1 update_error_count_label() current_device = get_current_device() if current_device: error_folder = "скрины ошибок" os.makedirs(error_folder, exist_ok=True) timestamp = time.strftime("%Y-%m-%d_%H-%M-%S") screenshot_filename = os.path.join(error_folder, f"error_{timestamp}.png") try: current_device.screenshot(screenshot_filename) log_message(f"Скрин: {screenshot_filename}") except Exception as e: log_message(f"Ошибка скрина: {e}") error_count += 1 update_error_count_label() restart_scenario() return time.sleep(random.uniform(3, 5)) log_message("Пароль установлен, 'Пропустить' нажата 4 раза. Аккаунт создан.") def clear_app_data(package_name): d = get_current_device() if not d: log_message("Устройство не подключено.") return output = run_adb_command(f"adb -s {d.serial} shell pm clear {package_name}") if output and "Success" in output: log_message(f"Данные {package_name} очищены.") else: log_message(f"Не удалось очистить {package_name}.") def launch_and_interact(): try: d = get_current_device() if not d: return pkg = "com.dualspacepro.multispace" d.app_start(pkg) time.sleep(2) if d(resourceId=f"{pkg}:id/tvAgree").click_exists(1): log_message("'Start' нажата.") else: if d(resourceId="com.android.permissioncontroller:id/permission_allow_button").click_exists(1): log_message("'Allow' нажата.") else: log_message("Ни 'Start', ни 'Allow' не найдены.") if not d(resourceId="com.android.permissioncontroller:id/permission_allow_button").click_exists(1): if d(resourceId=f"{pkg}:id/tvAgree").click_exists(1): log_message("'Start' нажата во второй раз.") else: log_message("Ни 'Start', ни 'Allow' не найдены.") except Exception as e: log_message(f"Ошибка запуска: {e}") def restart_scenario(): log_message("Перезапуск сценария.") global fetched_phone_number, fetched_activation_id, fetched_sms_code, fetched_password, scenario_restarts scenario_restarts += 1 update_scenario_restarts_label() fetched_phone_number = fetched_activation_id = fetched_sms_code = fetched_password = None set_current_country_label(primary_country_var.get()) update_price_and_count() log_message("Перезапущен.") def update_numbers_received_label(): root.after(0, lambda: numbers_received_label.config(text=f"Номеров получено: {numbers_received}")) update_difference_label() def update_numbers_canceled_label(): root.after(0, lambda: numbers_canceled_label.config(text=f"Номеров отменено: {numbers_canceled}")) update_difference_label() def update_sms_codes_received_label(): root.after(0, lambda: sms_codes_received_label.config(text=f"Сколько смс кодов: {sms_codes_received}")) def update_scenario_restarts_label(): root.after(0, lambda: scenario_restarts_label.config(text=f"Перезапусков: {scenario_restarts}")) def update_total_scenarios_label(): root.after(0, lambda: scenario_total_count_label.config(text=f"Сценариев: {total_scenarios_count}")) def update_scenario_count_label(count): root.after(0, lambda: scenario_count_label.config(text=f"Аккаунтов создано: {count}")) update_difference_label() def set_current_country_label(country): root.after(0, lambda: current_country_label.config(text=f"Текущая страна: {country}")) def load_last_selected_countries(): try: with open(LAST_SELECTED_COUNTRIES_FILE, "r", encoding="utf-8") as f: lines = f.read().splitlines() if len(lines) >= 2 and lines[0] in COUNTRIES and lines[1] in COUNTRIES: return lines[0], lines[1] except: pass return COUNTRY_DEFAULT_PRIMARY, COUNTRY_DEFAULT_SECONDARY def save_selected_countries(): try: with open(LAST_SELECTED_COUNTRIES_FILE, "w", encoding="utf-8") as f: f.write(f"{primary_country_var.get()}\n{secondary_country_var.get()}\n") except Exception as e: log_message(f"Ошибка сохранения стран: {e}") def load_last_limit(): try: with open(LAST_LIMIT_FILE, "r", encoding="utf-8") as f: lines = f.read().splitlines() if len(lines) >= 8: return map(int, lines[:8]) except: pass return (DEFAULT_SCENARIO_LIMIT, DEFAULT_MAX_INDEX, DEFAULT_SMS_WAIT_TIME, DEFAULT_SMS_CANCEL_ATTEMPTS, DEFAULT_SMS_CANCEL_DELAY, DEFAULT_IP_UPDATE_TIME, DEFAULT_DELAY_AFTER_ACCOUNT_FROM, DEFAULT_DELAY_AFTER_ACCOUNT_TO) def save_last_limit(): try: with open(LAST_LIMIT_FILE, "w", encoding="utf-8") as f: f.write(f"{scenario_limit_var.get() or DEFAULT_SCENARIO_LIMIT}\n{max_index_var.get()}\n{sms_wait_time_var.get() or DEFAULT_SMS_WAIT_TIME}\n{sms_cancel_attempts_var.get() or DEFAULT_SMS_CANCEL_ATTEMPTS}\n{sms_cancel_delay_var.get() or DEFAULT_SMS_CANCEL_DELAY}\n{ip_update_time_var.get() or DEFAULT_IP_UPDATE_TIME}\n{delay_after_account_from_var.get() or DEFAULT_DELAY_AFTER_ACCOUNT_FROM}\n{delay_after_account_to_var.get() or DEFAULT_DELAY_AFTER_ACCOUNT_TO}\n") except Exception as e: log_message(f"Ошибка сохранения лимитов: {e}") def load_last_limit_ip(): try: with open(LAST_LIMIT_IP_FILE, "r", encoding="utf-8") as f: lines = f.read().splitlines() if len(lines) >= 4: return map(int, lines[:4]) except: pass return 1, 2, 90, 120 def save_last_limit_ip(): try: with open(LAST_LIMIT_IP_FILE, "w", encoding="utf-8") as f: f.write(f"{ip_change_limit_var.get()}\n{sms_fail_limit_var.get()}\n{delay_after_ip_from_var.get()}\n{delay_after_ip_to_var.get()}\n") except Exception as e: log_message(f"Ошибка сохранения лимитов IP: {e}") def toggle_provider_labels(): provider = provider_var.get() if provider in ["SMSBower", "AliSMS"]: primary_provider_label.grid(row=1, column=4, padx=5, pady=5, sticky="w") secondary_provider_label.grid(row=2, column=4, padx=5, pady=5, sticky="w") else: primary_provider_label.grid_remove() secondary_provider_label.grid_remove() def update_price_and_count(event=None): selected_primary = primary_country_var.get() selected_secondary = secondary_country_var.get() provider = provider_var.get() toggle_provider_labels() fetch_country_price_and_count(COUNTRIES.get(selected_primary, "52"), "primary", provider) fetch_country_price_and_count(COUNTRIES.get(selected_secondary, "143"), "secondary", provider) save_selected_countries() get_balance_async() def get_proxy_ip(): try: proxies = {"http": PROXY_URL, "https": PROXY_URL} response = requests.get("https://api.ipify.org?format=json", proxies=proxies, timeout=10) if response.status_code == 200: return response.json().get("ip", "Нет данных") log_message(f"Ошибка IP: {response.status_code}") return "Ошибка" except Exception as e: log_message(f"Ошибка IP: {e}") return "Ошибка" def get_location_info(): try: proxies = {"http": PROXY_URL, "https": PROXY_URL} response = requests.get("http://ip-api.com/json", proxies=proxies, timeout=10) if response.status_code == 200: data = response.json() if data.get("status") == "success": return f"{data.get('country','Нет')}, {data.get('city','Нет')} - {data.get('isp','Нет')}" else: log_message("Ошибка локации: " + data.get("message", "")) return "Ошибка" else: log_message(f"Ошибка локации: {response.status_code}") return "Ошибка" except Exception as e: log_message(f"Ошибка локации: {e}") return "Ошибка" def fetch_country_price_and_count(country_code, category, provider): def task(): global primary_provider_data, secondary_provider_data try: if provider == "9GrizzlySMS": url = f"https://api.9grizzlysms.com/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getPrices&service={SERVICE}&country={country_code}" elif provider == "SMSMan": url = f"https://api.sms-man.com/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getPrices&service={SERVICE}&country={country_code}" elif provider == "AliSMS": url = f"https://api.alisms.org/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getPrices&service={SERVICE}&country={country_code}" else: url = f"https://smsbower.online/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getPricesV3&service={SERVICE}&country={country_code}" response = retry_session.get(url, timeout=10) if response.status_code == 200: data = response.json() if provider in ["9GrizzlySMS", "SMSMan"]: if SERVICE in data.get(country_code, {}) or SERVICE in data: cost = data.get(country_code, data).get(SERVICE, {}).get('cost', 'N/A') count = data.get(country_code, data).get(SERVICE, {}).get('count', 'N/A') set_price_and_count_labels(cost, count, category) if category == "primary": primary_provider_data[country_code] = [] else: secondary_provider_data[country_code] = [] else: set_price_and_count_labels("Нет", "Нет", category) if category == "primary": primary_provider_data[country_code] = [] else: secondary_provider_data[country_code] = [] elif provider in ["SMSBower", "AliSMS"]: if provider == "SMSBower": if country_code in data and SERVICE in data[country_code]: providers = data[country_code][SERVICE] else: providers = {} else: # AliSMS if SERVICE in data and country_code in data[SERVICE]: providers = data[SERVICE][country_code] else: providers = {} if providers: if isinstance(providers, dict): provider_list = [] costs = [] counts = [] for provider_id, provider_data in providers.items(): if isinstance(provider_data, dict) and 'price' in provider_data and 'count' in provider_data: costs.append(float(provider_data['price'])) counts.append(int(provider_data['count'])) provider_list.append({'id': provider_id, 'price': provider_data['price'], 'count': provider_data['count'], 'provider_id': provider_data.get('provider_id', provider_id)}) else: # For AliSMS, it might be direct price costs.append(float(provider_data)) counts.append(1) # Assume count 1 if not provided provider_list.append({'id': provider_id, 'price': provider_data, 'count': 1, 'provider_id': provider_id}) if category == "primary": primary_provider_data[country_code] = provider_list else: secondary_provider_data[country_code] = provider_list selected_provider = primary_selected_provider.get() if category == "primary" else secondary_selected_provider.get() if selected_provider: for p in provider_list: if p['id'] == selected_provider: set_price_and_count_labels(p['price'], p['count'], category) break else: set_price_and_count_labels("N/A", "N/A", category) else: cost = min(costs) if costs else 'N/A' count = sum(counts) if counts else 'N/A' set_price_and_count_labels(cost, count, category) else: set_price_and_count_labels("Нет", "Нет", category) if category == "primary": primary_provider_data[country_code] = [] else: secondary_provider_data[country_code] = [] else: set_price_and_count_labels("Нет", "Нет", category) if category == "primary": primary_provider_data[country_code] = [] else: secondary_provider_data[country_code] = [] else: set_price_and_count_labels("Ошибка", "Ошибка", category) except Exception as e: set_price_and_count_labels("Ошибка", "Ошибка", category) threading.Thread(target=task, daemon=True).start() def set_price_and_count_labels(cost, count, category): def update(): if category == "primary": price_label_primary.config(text=f"Цена: {cost}") count_label_primary.config(text=f"Кол-во: {count}") else: price_label_secondary.config(text=f"Цена: {cost}") count_label_secondary.config(text=f"Кол-во: {count}") root.after(0, update) def reset_statistics(): global scenario_run_count, numbers_received, numbers_canceled, sms_codes_received, scenario_restarts, total_scenarios_count, error_count scenario_run_count = numbers_received = numbers_canceled = sms_codes_received = scenario_restarts = total_scenarios_count = error_count = 0 update_scenario_count_label(scenario_run_count) update_numbers_received_label() update_numbers_canceled_label() update_sms_codes_received_label() update_scenario_restarts_label() update_total_scenarios_label() update_error_count_label() reset_timer() scan_devices() cancellation_tasks.clear() for item in cancellation_tree.get_children(): cancellation_tree.delete(item) log_message("Статистика сброшена.") def get_balance_async(): with semaphore: def task(): global balance_logged, low_balance_flag provider = provider_var.get() if provider == "9GrizzlySMS": url = f"https://api.9grizzlysms.com/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getBalance" elif provider == "SMSMan": url = f"https://api.sms-man.com/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getBalance" elif provider == "AliSMS": url = f"https://api.alisms.org/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getBalance" else: url = f"https://smsbower.online/stubs/handler_api.php?api_key={API_KEYS[provider]}&action=getBalance" try: response = retry_session.get(url, timeout=10) if response.status_code == 200: data = response.text.strip() if data.startswith("ACCESS_BALANCE"): parts = data.split(":") if len(parts) == 2: balance = parts[1] bal_val = float(balance) if not balance_logged: log_message(f"Баланс ({provider}): {balance}") balance_logged = True update_balance_label(balance) low_threshold = low_balance_threshold_var.get() if bal_val < low_threshold and not low_balance_flag: log_message(f"Баланс < {low_threshold}. Завершение после сценария.") low_balance_flag = True disable_start_button() elif bal_val >= low_threshold and low_balance_flag: log_message(f"Баланс >= {low_threshold}. Разблокировка.") low_balance_flag = False enable_start_button() else: update_balance_label("Некорректный формат") elif data == "BAD_KEY": update_balance_label("Ошибка: BAD_KEY") else: update_balance_label("Неизвестный ответ") else: update_balance_label(f"Ошибка {response.status_code}") except Exception as e: update_balance_label("Ошибка запроса") threading.Thread(target=task, daemon=True).start() def update_balance_label(balance): root.after(0, lambda: balance_label.config(text=f"Баланс: {balance}")) def periodic_balance_update(): def update(): get_balance_async() root.after(30000, update) update() def update_timer(): global total_time_elapsed if running: total_time_elapsed += 1 hrs, rem = divmod(total_time_elapsed, 3600) mins, secs = divmod(rem, 60) total_time_label.config(text=f"Время: {hrs:02}:{mins:02}:{secs:02}") root.after(1000, update_timer) def reset_timer(): global total_time_elapsed total_time_elapsed = 0 total_time_label.config(text="Время: 00:00:00") def disable_start_button(): start_button.config(state=tk.DISABLED) log_message("Запуск заблокирован (низкий баланс).") def enable_start_button(): start_button.config(state=tk.NORMAL) log_message("Запуск разблокирован.") def update_thread_count_label(): root.after(0, lambda: thread_count_label.config(text=f"Потоков: {active_threads}")) def click_back_button(): check_stop() d = get_current_device() if not d: log_message("Устройство не подключено.") return toolbar = d(resourceId="com.dualspacepro.multispace:id/toolbar") if toolbar.exists(timeout=5): back = toolbar.child(className="android.widget.ImageButton", index=1) if back.exists(timeout=5): back.click() log_message("'Назад' нажата.") else: log_message("'Назад' не найдена.") else: log_message("Toolbar не найден.") def update_cancellation_details(): selected = cancellation_tree.selection() if selected: try: task_id = int(selected[0]) if task_id in cancellation_tasks: log_data = cancellation_tasks[task_id]["log"] cancellation_details_text.config(state='normal') cancellation_details_text.delete("1.0", tk.END) cancellation_details_text.insert(tk.END, log_data) cancellation_details_text.see(tk.END) cancellation_details_text.config(state='disabled') except: pass root.after(1000, update_cancellation_details) def run_actions(): global stop_requested, scenario_run_count, running, active_threads, total_scenarios_count, stop_after_scenario, sms_fail_count, accounts_since_last_ip with active_threads_lock: active_threads += 1 update_thread_count_label() ip_change_limit = ip_change_limit_var.get() sms_fail_limit = sms_fail_limit_var.get() while not stop_requested and scenario_run_count < scenario_limit_var.get() and not low_balance_flag: log_message(f"Лимит: {scenario_limit_var.get()}. Создано: {scenario_run_count}") try: total_scenarios_count += 1 update_total_scenarios_label() launch_phone_app() d = get_current_device() if not d: log_message("Нет устройства.") break click_first_element() scroll_down() click_element_with_vk_text() click_clone_button() log_message(f"Ожидание {DELAY_BEFORE_LAST_ELEMENT} сек...") time.sleep(DELAY_BEFORE_LAST_ELEMENT) click_last_element() request_number() if not handle_sms_verification(): sms_fail_count += 1 remaining_sms_fail = sms_fail_limit - sms_fail_count remaining_sms_fail_label.config(text=f"{remaining_sms_fail}") if sms_fail_count >= sms_fail_limit: log_message("Лимит SMS, смена IP.") change_ip() sms_fail_count = 0 accounts_since_last_ip = 0 if stop_after_scenario: stop_requested = True break continue else: sms_fail_count = 0 remaining_sms_fail_label.config(text=str(sms_fail_limit_var.get())) input_first_last_name() select_gender(current_first_name) input_birthday() click_continue_button_after_birthday() input_password() log_message("Сценарий завершён.") current_dev = connected_devices[current_device_index] current_dev["accounts"] = current_dev.get("accounts", 0) + 1 scenario_run_count += 1 accounts_since_last_ip += 1 update_scenario_count_label(scenario_run_count) log_message(f"Создано: {scenario_run_count}/{scenario_limit_var.get()}") remaining_ip = ip_change_limit - accounts_since_last_ip remaining_ip_change_label.config(text=f"{remaining_ip} аккаунтов") if ip_change_limit and accounts_since_last_ip >= ip_change_limit: change_ip() accounts_since_last_ip = 0 launch_phone_app() delay_from = delay_after_account_from_var.get() delay_to = delay_after_account_to_var.get() random_delay = random.randint(delay_from, delay_to) log_message(f"Задержка: {random_delay} сек.") time.sleep(random_delay) update_devices_status_in_table() except RuntimeError as e: if str(e) == "Max index >= threshold, restarting.": log_message("max_index >= порога. Очистка и перезапуск.") clear_app_data("com.dualspacepro.multispace") launch_and_interact() continue elif str(e) == "Операция остановлена пользователем.": log_message("Остановлен пользователем.") cancel_activation() break else: log_message(str(e)) cancel_activation() break except Exception as e: log_message(f"Ошибка: {e}") cancel_activation() time.sleep(10) continue running = False log_message("Завершён.") root.after(0, lambda: start_button.config(state=tk.NORMAL)) root.after(0, lambda: stop_button.config(state=tk.DISABLED)) root.after(0, lambda: stop_after_button.config(state=tk.DISABLED)) root.after(0, lambda: change_ip_button.config(state=tk.NORMAL, bg="white", fg="#1ABC9C")) with active_threads_lock: active_threads -= 1 update_thread_count_label() def launch_and_click(): global worker_thread, stop_requested, running, stop_after_scenario with active_threads_lock: if active_threads > 0: log_message("Уже выполняется.") return start_button.config(state=tk.DISABLED) stop_button.config(state=tk.NORMAL) stop_after_button.config(state=tk.NORMAL) change_ip_button.config(state=tk.DISABLED, bg="#D3D3D3", fg="#888888") stop_after_scenario = False stop_requested = False running = True worker_thread = threading.Thread(target=run_actions, daemon=True) worker_thread.start() def stop_actions(): global stop_requested, running stop_requested = True running = False log_message("Остановка...") cancel_activation() root.after(0, lambda: start_button.config(state=tk.NORMAL)) root.after(0, lambda: stop_button.config(state=tk.DISABLED)) root.after(0, lambda: stop_after_button.config(state=tk.DISABLED)) root.after(0, lambda: change_ip_button.config(state=tk.NORMAL, bg="white", fg="#1ABC9C")) def stop_after_current_scenario(): global stop_after_scenario stop_after_scenario = True log_message("Остановка после сценария.") stop_after_button.config(state=tk.DISABLED) def delete_selected_cancellation_tasks(): selected_items = cancellation_tree.selection() for item in selected_items: cancellation_tree.delete(item) try: task_id = int(item) if task_id in cancellation_tasks: del cancellation_tasks[task_id] except: pass def select_all_cancellation_tasks(): all_items = cancellation_tree.get_children() cancellation_tree.selection_set(all_items) def cancellation_tree_context_menu(event): menu = tk.Menu(root, tearoff=0) menu.add_command(label="Удалить", command=delete_selected_cancellation_tasks) menu.add_command(label="Выбрать все", command=select_all_cancellation_tasks) menu.post(event.x_root, event.y_root) root = tk.Tk() root.withdraw() root.title("Авторегистратор ВК") root.geometry("1200x700") root.resizable(True, True) root.protocol("WM_DELETE_WINDOW", lambda: (messagebox.askokcancel("Выход", "Выйти?") and (stop_actions(), release_singleton(), root.destroy()))) primary_selected_provider = tk.StringVar(value="") secondary_selected_provider = tk.StringVar(value="") style = ttk.Style(root) style.theme_use('clam') BACKGROUND_COLOR = "#1E1E1E" # Темный фон для современного вида FRAME_COLOR = "#2A2A2A" BUTTON_COLOR = "#4CAF50" # Зеленый для кнопок BUTTON_FOREGROUND = "#FFFFFF" LABEL_COLOR = "#FFFFFF" TEXT_BACKGROUND = "#333333" TEXT_FOREGROUND = "#FFFFFF" root.configure(bg=BACKGROUND_COLOR) auto_scroll_var = tk.BooleanVar(value=True) show_browser_var = tk.BooleanVar(value=False) auto_launch_var = tk.BooleanVar(value=False) low_balance_threshold_var = tk.DoubleVar(value=load_balance_threshold()) # <--- ИЗМЕНЕНИЕ header_frame = tk.Frame(root, bg=BUTTON_COLOR, bd=2, relief=tk.RIDGE) header_frame.grid(row=0, column=0, columnspan=6, pady=(0,5), sticky="ew") header_title = tk.Label(header_frame, text="Авторегистратор ВК", font=("Helvetica", 24, "bold"), fg="white", bg=BUTTON_COLOR) header_title.pack(side=tk.LEFT, padx=20, pady=10) header_info = tk.Frame(header_frame, bg=BUTTON_COLOR) header_info.pack(side=tk.RIGHT, padx=20, pady=10) ip_frame = tk.Frame(header_info, bg=BUTTON_COLOR) ip_frame.grid(row=0, column=0, sticky="e", padx=5, pady=5) proxy_ip_label = tk.Label(ip_frame, text="IP: Получение...", font=("Helvetica", 12), fg="white", bg=BUTTON_COLOR) proxy_ip_label.pack(anchor="e") location_label = tk.Label(ip_frame, text="Локация: Получение...", font=("Helvetica", 12), fg="white", bg=BUTTON_COLOR) location_label.pack(anchor="e") button_frame = tk.Frame(header_info, bg=BUTTON_COLOR) button_frame.grid(row=0, column=1, sticky="e", padx=5, pady=5) update_ip_button = tk.Button(button_frame, text="Обновить IP", command=lambda: threading.Thread(target=update_ip_now, daemon=True).start(), font=("Helvetica", 10), bg="white", fg=BUTTON_COLOR) update_ip_button.pack(side="top", padx=5, pady=2) change_ip_button = tk.Button(button_frame, text="Сменить IP", command=lambda: threading.Thread(target=change_ip_manual, daemon=True).start(), font=("Helvetica", 10), bg="white", fg=BUTTON_COLOR) change_ip_button.pack(side="top", padx=5, pady=2) check_frame = tk.Frame(header_info, bg=BUTTON_COLOR) check_frame.grid(row=0, column=2, sticky="e", padx=5, pady=5) show_browser_check = tk.Checkbutton(check_frame, text="Показывать браузер при IP", variable=show_browser_var, font=("Helvetica", 10), bg=BUTTON_COLOR, fg="white", selectcolor=BUTTON_COLOR, activebackground=BUTTON_COLOR, activeforeground="white") show_browser_check.pack(side="top", padx=5, pady=2) auto_launch_check = tk.Checkbutton(check_frame, text="Автозапуск после IP", variable=auto_launch_var, font=("Helvetica", 10), bg=BUTTON_COLOR, fg="white", selectcolor=BUTTON_COLOR, activebackground=BUTTON_COLOR, activeforeground="white") auto_launch_check.pack(side="top", padx=5, pady=2) def update_ip_now(): def _update(): ip = get_proxy_ip() current = time.strftime("%H:%M:%S") root.after(0, lambda: proxy_ip_label.config(text=f"IP: {ip} ({current})")) loc = get_location_info() root.after(0, lambda: location_label.config(text=f"Локация: {loc}")) threading.Thread(target=_update, daemon=True).start() def save_auto_launch_state(): config = {"auto_launch": auto_launch_var.get()} with open("config.json", "w") as config_file: json.dump(config, config_file) auto_launch_var.trace("w", lambda *args: save_auto_launch_state()) try: with open("config.json", "r") as config_file: config = json.load(config_file) auto_launch_var.set(config.get("auto_launch", False)) except: pass def launch_device_screen(serial, width, height): try: process = subprocess.Popen(["scrcpy", "-s", serial, "--window-x", "0", "--window-y", "0", "--window-width", str(width), "--window-height", str(height)]) log_message(f"scrcpy для {serial} запущен.") time.sleep(DELAY_AFTER_SCRCPY) return process except Exception as e: log_message(f"Ошибка scrcpy для {serial}: {e}") return None def toggle_active_device_screen(): global scrcpy_processes, active_device_screen_enabled if not connected_devices: messagebox.showinfo("Смартфон", "Нет устройства.") return active_dev = connected_devices[current_device_index] serial = active_dev['serial'] if serial in scrcpy_processes: try: scrcpy_processes[serial].terminate() scrcpy_processes[serial].wait(timeout=5) log_message(f"scrcpy для {serial} завершён.") except Exception as e: log_message(f"Ошибка: {e}") del scrcpy_processes[serial] active_toggle_button.config(text="Показать") active_device_screen_enabled = False else: process = launch_device_screen(serial, 500, 1000) if process: scrcpy_processes[serial] = process active_toggle_button.config(text="Скрыть") active_device_screen_enabled = True else: messagebox.showerror("Ошибка", f"Не удалось для {serial}.") def toggle_devices_screen(): global scrcpy_processes selections = smartphones_tree.selection() if not selections: messagebox.showinfo("Выбор", "Выберите устройство.") return for item_id in selections: item = smartphones_tree.item(item_id) serial = item["values"][1] if serial in scrcpy_processes: try: scrcpy_processes[serial].terminate() scrcpy_processes[serial].wait(timeout=5) log_message(f"scrcpy для {serial} завершён.") except Exception as e: log_message(f"Ошибка: {e}") del scrcpy_processes[serial] else: process = launch_device_screen(serial, 500, 1000) if process: scrcpy_processes[serial] = process else: messagebox.showerror("Ошибка", f"Не удалось для {serial}.") update_devices_display_status() main_settings_frame = tk.LabelFrame(root, text="Настройки", font=("Helvetica",12,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR, labelanchor='nw') main_settings_frame.grid(row=1, column=0, columnspan=6, padx=10, pady=5, sticky="ew") country_frame = tk.LabelFrame(main_settings_frame, text="Страны", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR, labelanchor='nw') country_frame.grid(row=0, column=0, padx=5, pady=5, sticky="nw") tk.Label(country_frame, text="Провайдер:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=0, column=0, padx=5, pady=5, sticky="w") provider_var = tk.StringVar(value="9GrizzlySMS") load_last_provider() provider_var.trace_add("write", lambda *args: (save_last_provider(), check_api_key_and_toggle_start())) provider_combobox = ttk.Combobox(country_frame, textvariable=provider_var, state="readonly", font=("Helvetica",10), values=["9GrizzlySMS", "SMSBower", "SMSMan", "AliSMS"]) provider_combobox.grid(row=0, column=1, padx=5, pady=5, sticky="w") provider_combobox.bind("<>", update_price_and_count) primary_selected, secondary_selected = load_last_selected_countries() tk.Label(country_frame, text="Первая:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=1, column=0, padx=5, pady=5, sticky="w") primary_country_var = tk.StringVar(value=primary_selected) primary_country_combobox = ttk.Combobox(country_frame, textvariable=primary_country_var, state="readonly", font=("Helvetica",10), values=list(COUNTRIES.keys())) primary_country_combobox.grid(row=1, column=1, padx=5, pady=5, sticky="w") price_label_primary = tk.Label(country_frame, text="Цена: ?", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) price_label_primary.grid(row=1, column=2, padx=5, pady=5, sticky="w") count_label_primary = tk.Label(country_frame, text="Кол-во: ?", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) count_label_primary.grid(row=1, column=3, padx=5, pady=5, sticky="w") primary_provider_label = tk.Label(country_frame, text="Провайдер: Не выбран", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) primary_provider_label.grid(row=1, column=4, padx=5, pady=5, sticky="w") tk.Label(country_frame, text="Вторая:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=2, column=0, padx=5, pady=5, sticky="w") secondary_country_var = tk.StringVar(value=secondary_selected) secondary_country_combobox = ttk.Combobox(country_frame, textvariable=secondary_country_var, state="readonly", font=("Helvetica",10), values=list(COUNTRIES.keys())) secondary_country_combobox.grid(row=2, column=1, padx=5, pady=5, sticky="w") price_label_secondary = tk.Label(country_frame, text="Цена: ?", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) price_label_secondary.grid(row=2, column=2, padx=5, pady=5, sticky="w") count_label_secondary = tk.Label(country_frame, text="Кол-во: ?", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) count_label_secondary.grid(row=2, column=3, padx=5, pady=5, sticky="w") secondary_provider_label = tk.Label(country_frame, text="Провайдер: Не выбран", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) secondary_provider_label.grid(row=2, column=4, padx=5, pady=5, sticky="w") current_country_label = tk.Label(country_frame, text="Текущая: Не выбрана", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) current_country_label.grid(row=3, column=0, columnspan=5, padx=5, pady=5, sticky="w") primary_country_combobox.bind("", lambda e: show_provider_menu(e, primary_country_combobox, "primary")) secondary_country_combobox.bind("", lambda e: show_provider_menu(e, secondary_country_combobox, "secondary")) primary_country_combobox.bind("<>", update_price_and_count) secondary_country_combobox.bind("<>", update_price_and_count) scenario_frame = tk.LabelFrame(main_settings_frame, text="Сценарий", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR, labelanchor='nw') scenario_frame.grid(row=0, column=1, padx=5, pady=5, sticky="nw") default_limit, default_max_index, default_sms_wait_time, default_sms_cancel_attempts, default_sms_cancel_delay, default_ip_update_time, default_delay_after_account_from, default_delay_after_account_to = load_last_limit() tk.Label(scenario_frame, text="Лимит аккаунтов:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=0, column=0, padx=5, pady=5, sticky="e") scenario_limit_var = tk.IntVar(value=default_limit) ttk.Spinbox(scenario_frame, from_=1, to=999, textvariable=scenario_limit_var, width=5, font=("Helvetica",10)).grid(row=0, column=1, padx=5, pady=5, sticky="w") tk.Label(scenario_frame, text="Макс индекс:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=1, column=0, padx=5, pady=5, sticky="e") max_index_var = tk.IntVar(value=default_max_index) ttk.Spinbox(scenario_frame, from_=1, to=1000, textvariable=max_index_var, width=5, font=("Helvetica",10)).grid(row=1, column=1, padx=5, pady=5, sticky="w") tk.Label(scenario_frame, text="Обновление IP (сек):", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=2, column=0, padx=5, pady=5, sticky="e") ip_update_time_var = tk.IntVar(value=default_ip_update_time) ttk.Spinbox(scenario_frame, from_=1, to=3600, textvariable=ip_update_time_var, width=5, font=("Helvetica",10)).grid(row=2, column=1, padx=5, pady=5, sticky="w") scenario_limit_var.trace_add("write", lambda *args: save_last_limit()) max_index_var.trace_add("write", lambda *args: save_last_limit()) ip_update_time_var.trace_add("write", lambda *args: save_last_limit()) account_delay_frame = tk.LabelFrame(scenario_frame, text="Задержка аккаунт", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR, labelanchor='nw') account_delay_frame.grid(row=3, column=0, columnspan=2, padx=5, pady=5, sticky="nw") tk.Label(account_delay_frame, text="От:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=0, column=0, padx=5, pady=5, sticky="w") delay_after_account_from_var = tk.IntVar(value=default_delay_after_account_from) ttk.Spinbox(account_delay_frame, from_=1, to=3600, textvariable=delay_after_account_from_var, width=5, font=("Helvetica",10)).grid(row=0, column=1, padx=5, pady=5, sticky="w") tk.Label(account_delay_frame, text="До:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=0, column=2, padx=5, pady=5, sticky="w") delay_after_account_to_var = tk.IntVar(value=default_delay_after_account_to) ttk.Spinbox(account_delay_frame, from_=1, to=3600, textvariable=delay_after_account_to_var, width=5, font=("Helvetica",10)).grid(row=0, column=3, padx=5, pady=5, sticky="w") delay_after_account_from_var.trace_add("write", lambda *args: save_last_limit()) delay_after_account_to_var.trace_add("write", lambda *args: save_last_limit()) sms_frame = tk.LabelFrame(main_settings_frame, text="SMS", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR, labelanchor='nw') sms_frame.grid(row=0, column=2, padx=5, pady=5, sticky="nw") tk.Label(sms_frame, text="Ожидание SMS (сек):", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=0, column=0, padx=5, pady=5, sticky="w") sms_wait_time_var = tk.IntVar(value=default_sms_wait_time) ttk.Spinbox(sms_frame, from_=10, to=300, textvariable=sms_wait_time_var, width=5, font=("Helvetica",10)).grid(row=0, column=1, padx=5, pady=5, sticky="w") tk.Label(sms_frame, text="Попыток отмены:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=1, column=0, padx=5, pady=5, sticky="w") sms_cancel_attempts_var = tk.IntVar(value=default_sms_cancel_attempts) ttk.Spinbox(sms_frame, from_=1, to=10, textvariable=sms_cancel_attempts_var, width=5, font=("Helvetica",10)).grid(row=1, column=1, padx=5, pady=5, sticky="w") tk.Label(sms_frame, text="Задержка отмен (сек):", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=2, column=0, padx=5, pady=5, sticky="w") sms_cancel_delay_var = tk.IntVar(value=default_sms_cancel_delay) ttk.Spinbox(sms_frame, from_=1, to=60, textvariable=sms_cancel_delay_var, width=5, font=("Helvetica",10)).grid(row=2, column=1, padx=5, pady=5, sticky="w") sms_wait_time_var.trace_add("write", lambda *args: save_last_limit()) sms_cancel_attempts_var.trace_add("write", lambda *args: save_last_limit()) sms_cancel_delay_var.trace_add("write", lambda *args: save_last_limit()) limit_ip_frame = tk.LabelFrame(main_settings_frame, text="Лимит IP", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR, labelanchor='nw') limit_ip_frame.grid(row=2, column=0, columnspan=3, padx=5, pady=5, sticky="ew") default_ip_change_limit, default_sms_fail_limit, default_delay_after_ip_from, default_delay_after_ip_to = load_last_limit_ip() tk.Label(limit_ip_frame, text="Аккаунтов:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=0, column=0, padx=5, pady=5, sticky="w") ip_change_limit_var = tk.IntVar(value=default_ip_change_limit) ttk.Spinbox(limit_ip_frame, from_=1, to=999, textvariable=ip_change_limit_var, width=5, font=("Helvetica",10)).grid(row=0, column=1, padx=5, pady=5, sticky="w") tk.Label(limit_ip_frame, text="Осталось до IP:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=0, column=2, padx=5, pady=5, sticky="w") remaining_ip_change_label = tk.Label(limit_ip_frame, text="?", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) remaining_ip_change_label.grid(row=0, column=3, padx=5, pady=5, sticky="w") tk.Label(limit_ip_frame, text="Не пришедших SMS:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=1, column=0, padx=5, pady=5, sticky="w") sms_fail_limit_var = tk.IntVar(value=default_sms_fail_limit) ttk.Spinbox(limit_ip_frame, from_=1, to=10, textvariable=sms_fail_limit_var, width=5, font=("Helvetica",10)).grid(row=1, column=1, padx=5, pady=5, sticky="w") tk.Label(limit_ip_frame, text="Осталось до IP:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=1, column=2, padx=5, pady=5, sticky="w") remaining_sms_fail_label = tk.Label(limit_ip_frame, text="?", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) remaining_sms_fail_label.grid(row=1, column=3, padx=5, pady=5, sticky="w") tk.Label(limit_ip_frame, text="Смена смартфона (после N IP):", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=2, column=0, padx=5, pady=5, sticky="w") device_change_limit_var = tk.IntVar(value=2) ttk.Spinbox(limit_ip_frame, from_=1, to=99, textvariable=device_change_limit_var, width=5, font=("Helvetica",10)).grid(row=2, column=1, padx=5, pady=5, sticky="w") tk.Label(limit_ip_frame, text="Осталось до смартфона:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=2, column=2, padx=5, pady=5, sticky="w") remaining_device_change_label = tk.Label(limit_ip_frame, text="?", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) remaining_device_change_label.grid(row=2, column=3, padx=5, pady=5, sticky="w") tk.Label(limit_ip_frame, text="Задержка после IP (сек):", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=3, column=0, padx=5, pady=5, sticky="w") tk.Label(limit_ip_frame, text="От:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=3, column=1, padx=5, pady=5, sticky="w") delay_after_ip_from_var = tk.IntVar(value=default_delay_after_ip_from) ttk.Spinbox(limit_ip_frame, from_=1, to=3600, textvariable=delay_after_ip_from_var, width=5, font=("Helvetica",10)).grid(row=3, column=2, padx=5, pady=5, sticky="w") tk.Label(limit_ip_frame, text="До:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=3, column=3, padx=5, pady=5, sticky="w") delay_after_ip_to_var = tk.IntVar(value=default_delay_after_ip_to) ttk.Spinbox(limit_ip_frame, from_=1, to=3600, textvariable=delay_after_ip_to_var, width=5, font=("Helvetica",10)).grid(row=3, column=4, padx=5, pady=5, sticky="w") ip_change_limit_var.trace_add("write", lambda *args: save_last_limit_ip()) sms_fail_limit_var.trace_add("write", lambda *args: save_last_limit_ip()) delay_after_ip_from_var.trace_add("write", lambda *args: save_last_limit_ip()) delay_after_ip_to_var.trace_add("write", lambda *args: save_last_limit_ip()) stats_frame = tk.LabelFrame(main_settings_frame, text="Статистика", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR, labelanchor='nw') stats_frame.grid(row=1, column=0, columnspan=3, padx=5, pady=5, sticky="nw") balance_label = tk.Label(stats_frame, text="Баланс: ?", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) balance_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") thread_count_label = tk.Label(stats_frame, text="Потоков: 0", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) thread_count_label.grid(row=0, column=1, padx=5, pady=5, sticky="w") total_time_label = tk.Label(stats_frame, text="Время: 00:00:00", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) total_time_label.grid(row=0, column=2, padx=5, pady=5, sticky="w") active_device_model_label = tk.Label(stats_frame, text="Активный: Нет", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) active_device_model_label.grid(row=3, column=0, padx=5, pady=5, sticky="w") active_toggle_button = tk.Button(stats_frame, text="Показать устройство", command=lambda: threading.Thread(target=toggle_active_device_screen, daemon=True).start(), font=("Helvetica",10,"bold"), bg="white", fg=BUTTON_COLOR) active_toggle_button.grid(row=3, column=1, padx=5, pady=5, sticky="w") scenario_count_label = tk.Label(stats_frame, text="Аккаунтов: 0", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) scenario_count_label.grid(row=1, column=0, padx=5, pady=5, sticky="w") numbers_received_label = tk.Label(stats_frame, text="Номеров получено: 0", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) numbers_received_label.grid(row=1, column=1, padx=5, pady=5, sticky="w") numbers_canceled_label = tk.Label(stats_frame, text="Номеров отменено: 0", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) numbers_canceled_label.grid(row=1, column=2, padx=5, pady=5, sticky="w") sms_codes_received_label = tk.Label(stats_frame, text="СМС кодов: 0", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) sms_codes_received_label.grid(row=2, column=0, padx=5, pady=5, sticky="w") scenario_restarts_label = tk.Label(stats_frame, text="Перезапусков: 0", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) scenario_restarts_label.grid(row=2, column=1, padx=5, pady=5, sticky="w") scenario_total_count_label = tk.Label(stats_frame, text="Сценариев: 0", font=("Helvetica",10,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) scenario_total_count_label.grid(row=2, column=2, padx=5, pady=5, sticky="w") difference_label = tk.Label(stats_frame, text="Не отменён: 0", font=("Helvetica", 10, "bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) difference_label.grid(row=1, column=3, padx=5, pady=5, sticky="w") errors_count_label = tk.Label(stats_frame, text="Ошибок: 0", font=("Helvetica", 10, "bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) errors_count_label.grid(row=2, column=3, padx=5, pady=5, sticky="w") tk.Label(stats_frame, text="Порог баланса <", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=4, column=0, padx=5, pady=5, sticky="w") low_balance_threshold_spinbox = ttk.Spinbox(stats_frame, from_=0.01, to=1000.0, increment=0.01, format="%.2f", textvariable=low_balance_threshold_var, width=7, font=("Helvetica",10)) low_balance_threshold_spinbox.grid(row=4, column=1, padx=5, pady=5, sticky="w") # <--- ИЗМЕНЕНИЕ: Добавлено сохранение в trace low_balance_threshold_var.trace_add("write", lambda *args: (get_balance_async(), save_balance_threshold())) # <--- КОНЕЦ ИЗМЕНЕНИЯ buttons_frame = tk.Frame(root, bg=BACKGROUND_COLOR) buttons_frame.grid(row=2, column=0, columnspan=6, pady=5) start_button = tk.Button(buttons_frame, text="Запустить", command=launch_and_click, font=("Helvetica",12,"bold"), bg=BUTTON_COLOR, fg=BUTTON_FOREGROUND, bd=0, activebackground="#388E3C", width=10) start_button.grid(row=0, column=0, padx=10, pady=5) stop_button = tk.Button(buttons_frame, text="Остановить", command=stop_actions, font=("Helvetica",12,"bold"), bg="#F44336", fg=BUTTON_FOREGROUND, bd=0, activebackground="#D32F2F", width=10) stop_button.grid(row=0, column=1, padx=10, pady=5) stop_button.config(state=tk.DISABLED) stop_after_button = tk.Button(buttons_frame, text="После сценария", command=stop_after_current_scenario, font=("Helvetica",12,"bold"), bg="#FF9800", fg=BUTTON_FOREGROUND, bd=0, activebackground="#F57C00", width=15) stop_after_button.grid(row=0, column=2, padx=10, pady=5) stop_after_button.config(state=tk.DISABLED) stats_reset_button = tk.Button(buttons_frame, text="Сброс статистики", command=reset_statistics, font=("Helvetica",12,"bold"), bg="#9C27B0", fg=BUTTON_FOREGROUND, bd=0, activebackground="#7B1FA2", width=15) stats_reset_button.grid(row=0, column=4, padx=10, pady=5) notebook = ttk.Notebook(root) notebook.grid(row=3, column=0, columnspan=6, padx=10, pady=5, sticky="nsew") root.grid_rowconfigure(3, weight=1) root.grid_columnconfigure(5, weight=1) def update_error_count_label(): root.after(0, lambda: errors_count_label.config(text=f"Ошибок: {error_count}")) def search_log(event=None): query = search_entry.get() log_text.tag_remove("search", "1.0", tk.END) log_text.tag_remove("current_search", "1.0", tk.END) global search_matches, search_current_index search_matches = [] search_current_index = 0 if not query: search_count_label.config(text="0") search_index_label.config(text="") return start_pos = "1.0" while True: pos = log_text.search(query, start_pos, stopindex=tk.END, nocase=True) if not pos: break end_pos = f"{pos}+{len(query)}c" log_text.tag_add("search", pos, end_pos) search_matches.append(pos) start_pos = end_pos count = len(search_matches) search_count_label.config(text=f"{count}") if count > 0: search_current_index = 0 highlight_current_match() else: search_index_label.config(text="") def highlight_current_match(): log_text.tag_remove("current_search", "1.0", tk.END) if search_matches: query = search_entry.get() pos = search_matches[search_current_index] end_pos = f"{pos}+{len(query)}c" log_text.tag_add("current_search", pos, end_pos) log_text.tag_config("current_search", background="orange") log_text.see(pos) search_index_label.config(text=f"{search_current_index+1}/{len(search_matches)}") def next_match(): global search_current_index if search_matches: search_current_index = (search_current_index + 1) % len(search_matches) highlight_current_match() def prev_match(): global search_current_index if search_matches: search_current_index = (search_current_index - 1) % len(search_matches) highlight_current_match() def separate_search_log(event=None): global separate_search_matches, separate_search_current_index separate_log_text.tag_remove("search", "1.0", tk.END) separate_log_text.tag_remove("current_search", "1.0", tk.END) query = separate_search_entry.get() if not query: separate_search_matches = [] separate_search_current_index = 0 separate_search_count_label.config(text="0") separate_search_index_label.config(text="") return start_pos = "1.0" separate_search_matches = [] while True: pos = separate_log_text.search(query, start_pos, stopindex=tk.END, nocase=True) if not pos: break end_pos = f"{pos}+{len(query)}c" separate_log_text.tag_add("search", pos, end_pos) separate_search_matches.append(pos) start_pos = end_pos count = len(separate_search_matches) separate_search_count_label.config(text=f"{count}") if count > 0: separate_search_current_index = 0 separate_highlight_current_match() else: separate_search_index_label.config(text="") def separate_highlight_current_match(): global separate_search_current_index separate_log_text.tag_remove("current_search", "1.0", tk.END) if separate_search_matches: query = separate_search_entry.get() pos = separate_search_matches[separate_search_current_index] end_pos = f"{pos}+{len(query)}c" separate_log_text.tag_add("current_search", pos, end_pos) separate_log_text.tag_config("current_search", background="orange") separate_log_text.see(pos) separate_search_index_label.config(text=f"{separate_search_current_index+1}/{len(separate_search_matches)}") def separate_next_match(): global separate_search_current_index if separate_search_matches: separate_search_current_index = (separate_search_current_index + 1) % len(separate_search_matches) separate_highlight_current_match() def separate_prev_match(): global separate_search_current_index if separate_search_matches: separate_search_current_index = (separate_search_current_index - 1) % len(separate_search_matches) separate_highlight_current_match() journal_tab = tk.Frame(notebook, bg=FRAME_COLOR) notebook.add(journal_tab, text="Журнал") search_frame = tk.Frame(journal_tab, bg=FRAME_COLOR) search_frame.pack(fill=tk.X, padx=5, pady=5) tk.Label(search_frame, text="Поиск:", font=("Helvetica", 10), bg=FRAME_COLOR, fg=LABEL_COLOR).pack(side=tk.LEFT, padx=(0, 5)) search_entry = tk.Entry(search_frame, font=("Helvetica", 10)) search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5)) search_entry.bind("", search_log) open_window_button = tk.Button(search_frame, text="Отдельное окно", command=open_separate_log_window, bg=BUTTON_COLOR, fg=BUTTON_FOREGROUND, font=("Helvetica", 10)) open_window_button.pack(side=tk.LEFT, padx=5) def search_entry_context_menu(event): menu = tk.Menu(root, tearoff=0, bg=FRAME_COLOR, fg=LABEL_COLOR) menu.add_command(label="Копировать", command=lambda: (root.clipboard_clear(), root.clipboard_append(search_entry.get()))) menu.add_command(label="Выделить всё", command=lambda: search_entry.select_range(0, tk.END)) menu.add_command(label="Вставить", command=lambda: search_entry.insert(tk.END, root.clipboard_get())) menu.post(event.x_root, event.y_root) search_entry.bind("", search_entry_context_menu) tk.Button(search_frame, text="Предыдущий", font=("Helvetica", 10), command=prev_match, bg=BUTTON_COLOR, fg=BUTTON_FOREGROUND).pack(side=tk.LEFT, padx=5) tk.Button(search_frame, text="Следующий", font=("Helvetica", 10), command=next_match, bg=BUTTON_COLOR, fg=BUTTON_FOREGROUND).pack(side=tk.LEFT, padx=5) search_index_label = tk.Label(search_frame, text="", font=("Helvetica", 10), bg=FRAME_COLOR, fg=LABEL_COLOR) search_index_label.pack(side=tk.LEFT, padx=5) search_count_label = tk.Label(search_frame, text="0", font=("Helvetica", 10), bg=FRAME_COLOR, fg=LABEL_COLOR) search_count_label.pack(side=tk.LEFT, padx=5) tk.Button(search_frame, text="Искать", font=("Helvetica", 10), command=search_log, bg=BUTTON_COLOR, fg=BUTTON_FOREGROUND).pack(side=tk.LEFT, padx=(0, 5)) log_text = scrolledtext.ScrolledText(journal_tab, state='disabled', wrap=tk.WORD, font=("Consolas", 10), bg=TEXT_BACKGROUND, fg=TEXT_FOREGROUND, insertbackground=LABEL_COLOR) log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) log_text.configure(borderwidth=2, relief="groove") def toggle_autoscroll(): auto_scroll_var.set(not auto_scroll_var.get()) def create_context_menu(event): menu = tk.Menu(root, tearoff=0, bg=FRAME_COLOR, fg=LABEL_COLOR) if log_text.tag_ranges(tk.SEL): menu.add_command(label="Копировать", command=lambda: copy_to_clipboard(log_text.get(tk.SEL_FIRST, tk.SEL_LAST))) menu.add_command(label="Выделить всё", command=lambda: (log_text.tag_add(tk.SEL, "1.0", tk.END), log_text.mark_set(tk.INSERT, "1.0"), log_text.see(tk.INSERT))) auto_scroll_label = "Отключить автопрокрутку" if auto_scroll_var.get() else "Включить автопрокрутку" menu.add_command(label=auto_scroll_label, command=toggle_autoscroll) menu.post(event.x_root, event.y_root) log_text.bind("", create_context_menu) log_text.bind("", create_context_menu) root.bind_all("", lambda e: None) root.bind_all("", lambda e: (log_text.tag_add(tk.SEL, "1.0", tk.END), "break")) cancel_tab = tk.Frame(notebook, bg=FRAME_COLOR) notebook.add(cancel_tab, text="Отмена SMS") cancel_table_frame = tk.Frame(cancel_tab, bg=FRAME_COLOR) cancel_table_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) cancel_table_frame.rowconfigure(0, weight=1) cancel_table_frame.columnconfigure(0, weight=1) columns = ("task_id", "phone", "status", "attempts", "start", "end") cancellation_tree = ttk.Treeview(cancel_table_frame, columns=columns, show="headings", selectmode="extended") cancellation_tree.heading("task_id", text="ID") cancellation_tree.heading("phone", text="Номер") cancellation_tree.heading("status", text="Статус") cancellation_tree.heading("attempts", text="Попытки") cancellation_tree.heading("start", text="Начало") cancellation_tree.heading("end", text="Конец") for col in columns: cancellation_tree.column(col, anchor="center", width=100) vsb = ttk.Scrollbar(cancel_table_frame, orient="vertical", command=cancellation_tree.yview) cancellation_tree.configure(yscrollcommand=vsb.set) cancellation_tree.grid(row=0, column=0, sticky="nsew") vsb.grid(row=0, column=1, sticky="ns") cancellation_tree.bind("", cancellation_tree_context_menu) cancel_details_frame = tk.Frame(cancel_tab, bg=FRAME_COLOR) cancel_details_frame.grid(row=0, column=1, sticky="nsew", padx=5, pady=5) cancel_tab.columnconfigure(0, weight=1) cancel_tab.columnconfigure(1, weight=1) cancel_tab.rowconfigure(0, weight=1) cancellation_details_text = scrolledtext.ScrolledText(cancel_details_frame, state='disabled', wrap=tk.WORD, font=("Consolas",10), bg=TEXT_BACKGROUND, fg=TEXT_FOREGROUND, insertbackground=LABEL_COLOR) cancellation_details_text.pack(fill=tk.BOTH, expand=True) def on_cancel_task_select(event): selected = cancellation_tree.selection() if selected: try: task_id = int(selected[0]) if task_id in cancellation_tasks: log_data = cancellation_tasks[task_id]["log"] cancellation_details_text.config(state='normal') cancellation_details_text.delete("1.0", tk.END) cancellation_details_text.insert(tk.END, log_data) cancellation_details_text.see(tk.END) cancellation_details_text.config(state='disabled') except: pass cancellation_tree.bind("<>", on_cancel_task_select) smartphones_tab = tk.Frame(notebook, bg=FRAME_COLOR) notebook.add(smartphones_tab, text="Смартфоны") left_frame = tk.Frame(smartphones_tab, bg=FRAME_COLOR) left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) phones_columns = ("id", "serial", "model", "status", "accounts", "numbers") smartphones_tree = ttk.Treeview(left_frame, columns=phones_columns, show="headings", selectmode="extended") smartphones_tree.tag_configure("shown", background="#A9DFBF") smartphones_tree.heading("id", text="ID") smartphones_tree.heading("serial", text="Serial") smartphones_tree.heading("model", text="Модель") smartphones_tree.heading("status", text="Статус") smartphones_tree.heading("accounts", text="Аккаунтов") smartphones_tree.heading("numbers", text="Номеров") for col in phones_columns: smartphones_tree.column(col, anchor="center", width=120) smartphones_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scroll_phones = ttk.Scrollbar(left_frame, orient=tk.VERTICAL, command=smartphones_tree.yview) smartphones_tree.configure(yscrollcommand=scroll_phones.set) scroll_phones.pack(side=tk.LEFT, fill=tk.Y) def copy_serial_number(): selection = smartphones_tree.selection() if selection: item_id = selection[0] item = smartphones_tree.item(item_id) serial = item["values"][1] root.clipboard_clear() root.clipboard_append(serial) log_message(f"Serial {serial} скопирован.") def smartphones_tree_context_menu(event): menu = tk.Menu(root, tearoff=0, bg=FRAME_COLOR, fg=LABEL_COLOR) menu.add_command(label="Копировать serial", command=copy_serial_number) menu.post(event.x_root, event.y_root) smartphones_tree.bind("", smartphones_tree_context_menu) right_frame = tk.Frame(smartphones_tab, bg=FRAME_COLOR) right_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) toggle_screen_button = tk.Button(right_frame, text="Показать/Скрыть", command=lambda: threading.Thread(target=toggle_devices_screen, daemon=True).start(), font=("Helvetica",10,"bold"), bg="white", fg=BUTTON_COLOR) toggle_screen_button.pack(pady=5) active_smartphone_label = tk.Label(right_frame, text="Активный: ---", font=("Helvetica",12,"bold"), fg=LABEL_COLOR, bg=FRAME_COLOR) active_smartphone_label.pack(pady=10) refresh_button = tk.Button(right_frame, text="Обновить", command=scan_devices, font=("Helvetica",10,"bold"), bg="white", fg=BUTTON_COLOR) refresh_button.pack(pady=5) select_as_first_button = tk.Button(right_frame, text="Выбрать первым", command=select_smartphone_as_first, font=("Helvetica",10,"bold"), bg="white", fg=BUTTON_COLOR) select_as_first_button.pack(pady=5) api_keys_tab = tk.Frame(notebook, bg=FRAME_COLOR) notebook.add(api_keys_tab, text="API Keys") api_entries = {} api_save_buttons = {} api_original_keys = {} def save_api_key(provider): key = api_entries[provider].get() file = API_KEY_FILES[provider] with open(file, "w", encoding="utf-8") as f: f.write(key) API_KEYS[provider] = key api_save_buttons[provider].config(state=tk.DISABLED) api_original_keys[provider] = key log_message(f"API ключ для {provider} сохранён.") get_balance_async() update_price_and_count() check_api_key_and_toggle_start() def enable_save_button(provider, event=None): current_key = api_entries[provider].get() if current_key != api_original_keys.get(provider, ""): api_save_buttons[provider].config(state=tk.NORMAL) else: api_save_buttons[provider].config(state=tk.DISABLED) def api_key_context_menu(event, entry): menu = tk.Menu(root, tearoff=0, bg=FRAME_COLOR, fg=LABEL_COLOR) menu.add_command(label="Копировать", command=lambda: copy_to_clipboard(entry.get())) menu.add_command(label="Вставить", command=lambda: entry.insert(tk.END, root.clipboard_get())) menu.add_command(label="Удалить", command=lambda: (entry.delete(0, tk.END), enable_save_button(entry.provider))) menu.post(event.x_root, event.y_root) for row, provider in enumerate(API_KEY_FILES.keys()): tk.Label(api_keys_tab, text=f"{provider}:", font=("Helvetica",10), fg=LABEL_COLOR, bg=FRAME_COLOR).grid(row=row, column=0, padx=5, pady=5, sticky="w") entry_var = tk.StringVar(value=API_KEYS[provider]) entry = tk.Entry(api_keys_tab, textvariable=entry_var, font=("Helvetica",10), width=50) entry.grid(row=row, column=1, padx=5, pady=5, sticky="w") entry.provider = provider api_entries[provider] = entry_var entry.bind("", lambda e, p=provider: enable_save_button(p, e)) entry_var.trace_add("write", lambda name, index, mode, p=provider: enable_save_button(p)) entry.bind("", lambda e, ent=entry: api_key_context_menu(e, ent)) save_btn = tk.Button(api_keys_tab, text="Сохранить", command=lambda p=provider: save_api_key(p), state=tk.DISABLED, font=("Helvetica",10), bg=BUTTON_COLOR, fg=BUTTON_FOREGROUND) save_btn.grid(row=row, column=2, padx=5, pady=5, sticky="w") api_save_buttons[provider] = save_btn api_original_keys[provider] = API_KEYS[provider] def check_api_key_and_toggle_start(): provider = provider_var.get() if not API_KEYS.get(provider): start_button.config(state=tk.DISABLED) else: start_button.config(state=tk.NORMAL) def set_tkinter_window_geometry(): screen_width = root.winfo_screenwidth() screen_height = root.winfo_screenheight() scrcpy_width = 500 gui_width = 1200 gui_height = 950 gui_x = scrcpy_width gui_y = 0 root.geometry(f"{gui_width}x{gui_height}+{gui_x}+{gui_y}") def on_start(): set_tkinter_window_geometry() threading.Thread(target=update_ip_now, daemon=True).start() periodic_balance_update() update_price_and_count() load_primary_provider() load_secondary_provider() update_timer() update_cancellation_details() scan_devices() check_api_key_and_toggle_start() def show_provider_menu(event, combobox, category): country = combobox.get() country_code = COUNTRIES.get(country, "52") provider = provider_var.get() if provider not in ["SMSBower", "AliSMS"]: return providers_data_source = primary_provider_data if category == "primary" else secondary_provider_data providers_for_country = providers_data_source.get(country_code, []) try: sorted_providers = sorted(providers_for_country, key=lambda p: int(p['id']) if str(p.get('id')).isdigit() else float('-inf'), reverse=True) except: sorted_providers = sorted(providers_for_country, key=lambda p: str(p.get('id', '')), reverse=True) menu = tk.Menu(root, tearoff=0, bg=FRAME_COLOR, fg=LABEL_COLOR) menu.add_command(label="Не выбирать", command=lambda: set_primary_provider("") if category == "primary" else set_secondary_provider("")) if sorted_providers: for p in sorted_providers: provider_id_display = p.get('id', 'N/A') price_display = p.get('price', 'N/A') count_display = p.get('count', 'N/A') actual_provider_id_display = p.get('provider_id', 'N/A') label = f"ID: {provider_id_display}, Цена: {price_display}, Кол-во: {count_display}, Провайдер: {actual_provider_id_display}" current_provider_key_for_command = p.get('id') if current_provider_key_for_command is not None: menu.add_command(label=label, command=lambda p_id_cmd=current_provider_key_for_command: (set_primary_provider(p_id_cmd) if category == "primary" else set_secondary_provider(p_id_cmd))) else: menu.add_command(label=f"Неполные данные: {p}", state=tk.DISABLED) else: menu.add_command(label="Нет провайдеров", state=tk.DISABLED) try: menu.post(event.x_root, event.y_root) except Exception as e: log_message(f"Ошибка меню: {e}") root.after(1000, on_start) show_splash_screen() root.mainloop()